|
13 | 13 | import time |
14 | 14 | from typing import TYPE_CHECKING, Any |
15 | 15 |
|
16 | | -from ..models._adapter_utils import FINISH_LENGTH, default_max_output_tokens |
| 16 | +from ..models._adapter_utils import ( |
| 17 | + FINISH_LENGTH, |
| 18 | + default_max_output_tokens, |
| 19 | + needs_reasoning_headroom, |
| 20 | +) |
17 | 21 | from ..models.base import BaseModel, GenerationConfig |
18 | 22 | from ..models.errors import ( |
19 | 23 | InvalidRequestError, |
|
35 | 39 | _TRUNCATION_ESCALATION_FACTOR = 4 |
36 | 40 | _TRUNCATION_MAX_TOKENS_CEILING = 8192 |
37 | 41 |
|
| 42 | +# A reasoning model asked for structured output needs headroom well beyond the |
| 43 | +# reasoning-family default (it spends budget on hidden reasoning *and* the |
| 44 | +# JSON/schema structure before any field value). Below this, warn at call time |
| 45 | +# instead of only after a billed, truncated failure. |
| 46 | +_REASONING_STRUCTURED_OUTPUT_MIN_TOKENS = 8192 |
| 47 | + |
38 | 48 | if TYPE_CHECKING: |
39 | 49 | pass |
40 | 50 |
|
41 | 51 | logger = logging.getLogger(__name__) |
42 | 52 | _slog = get_structured_logger(__name__) |
| 53 | +# (model_name, kind) pairs already warned about in this process — a call-time |
| 54 | +# reasoning-budget heads-up fires once per model/kind, not on every call. |
| 55 | +_reasoning_budget_warned: set[tuple[str, str]] = set() |
43 | 56 | # Canonical structured observability logger — emits redacted JSON lines with OTel context |
44 | 57 | _obs_log = _get_obs_logger(__name__) |
45 | 58 |
|
@@ -307,7 +320,12 @@ def _generate(self, prompt: Any, **kwargs) -> dict[str, Any]: |
307 | 320 | gen_config = GenerationConfig( |
308 | 321 | temperature=retry_temperature, |
309 | 322 | max_tokens=current_max_tokens, |
310 | | - top_p=kwargs.get('top_p', 0.9), |
| 323 | + top_p=kwargs.get('top_p', self.config.top_p), |
| 324 | + top_k=kwargs.get('top_k', self.config.top_k), |
| 325 | + seed=kwargs.get('seed', self.config.seed), |
| 326 | + presence_penalty=kwargs.get('presence_penalty', self.config.presence_penalty), |
| 327 | + frequency_penalty=kwargs.get('frequency_penalty', self.config.frequency_penalty), |
| 328 | + repetition_penalty=kwargs.get('repetition_penalty', self.config.repetition_penalty), |
311 | 329 | stop_sequences=kwargs.get('stop_sequences', default_stop_sequences) |
312 | 330 | ) |
313 | 331 |
|
@@ -483,6 +501,47 @@ def _record_provider_metrics( |
483 | 501 | output_tokens=int(completion_tokens or 0), |
484 | 502 | ) |
485 | 503 |
|
| 504 | + def _warn_reasoning_budget(self, max_tokens: int | None, structured_output: bool) -> None: |
| 505 | + """Warn once per model when a reasoning model's budget looks too tight. |
| 506 | +
|
| 507 | + gpt-5 / o-series models spend part of ``max_tokens`` on hidden |
| 508 | + reasoning before any visible token, so a small pinned budget — or a |
| 509 | + generous one paired with structured output, which needs headroom for |
| 510 | + the reasoning *and* the JSON/schema structure — can produce nothing |
| 511 | + and still be billed. Logged before the call so the caller sees it |
| 512 | + without first burning a failed, billed round-trip. |
| 513 | + """ |
| 514 | + model = self.model |
| 515 | + if model is None or not needs_reasoning_headroom(model): |
| 516 | + return |
| 517 | + name = getattr(model, "model_name", None) or self.model_name or "unknown" |
| 518 | + |
| 519 | + if structured_output: |
| 520 | + min_tokens = _REASONING_STRUCTURED_OUTPUT_MIN_TOKENS |
| 521 | + if max_tokens is not None and max_tokens >= min_tokens: |
| 522 | + return |
| 523 | + kind, hint = "structured", ( |
| 524 | + f"'{name}' is a reasoning model asked for structured output with " |
| 525 | + f"max_tokens={max_tokens if max_tokens is not None else 'default'} — " |
| 526 | + f"reasoning plus the JSON structure can consume the whole budget " |
| 527 | + f"before any field is filled. Consider max_tokens>={min_tokens}." |
| 528 | + ) |
| 529 | + else: |
| 530 | + min_tokens = default_max_output_tokens(model) |
| 531 | + if max_tokens is None or max_tokens >= min_tokens: |
| 532 | + return |
| 533 | + kind, hint = "tight_budget", ( |
| 534 | + f"'{name}' is a reasoning model with max_tokens={max_tokens} — it can " |
| 535 | + f"spend the whole budget on hidden reasoning and return no visible " |
| 536 | + f"text. Consider max_tokens>={min_tokens}." |
| 537 | + ) |
| 538 | + |
| 539 | + warn_key = (name, kind) |
| 540 | + if warn_key in _reasoning_budget_warned: |
| 541 | + return |
| 542 | + _reasoning_budget_warned.add(warn_key) |
| 543 | + logger.warning(hint) |
| 544 | + |
486 | 545 | def _truncation_error_detail(self, model: Any, max_tokens: int) -> dict[str, Any]: |
487 | 546 | """Structured error for a deterministic ``max_tokens`` truncation. |
488 | 547 |
|
@@ -705,7 +764,12 @@ def _generate_speculative(self, prompt: str, **kwargs) -> dict[str, Any] | None: |
705 | 764 | gen_config = GenerationConfig( |
706 | 765 | temperature=base_temperature, |
707 | 766 | max_tokens=kwargs.get('max_tokens', default_max_output_tokens(self.model)), |
708 | | - top_p=kwargs.get('top_p', 0.9), |
| 767 | + top_p=kwargs.get('top_p', self.config.top_p), |
| 768 | + top_k=kwargs.get('top_k', self.config.top_k), |
| 769 | + seed=kwargs.get('seed', self.config.seed), |
| 770 | + presence_penalty=kwargs.get('presence_penalty', self.config.presence_penalty), |
| 771 | + frequency_penalty=kwargs.get('frequency_penalty', self.config.frequency_penalty), |
| 772 | + repetition_penalty=kwargs.get('repetition_penalty', self.config.repetition_penalty), |
709 | 773 | stop_sequences=kwargs.get('stop_sequences', default_stop_sequences), |
710 | 774 | ) |
711 | 775 |
|
|
0 commit comments