bugfix: max_tokens hardcoded

This commit is contained in:
2026-09-19 00:30:51 -03:00
parent 42e1720d1c
commit 20a891b1d7
43 changed files with 464 additions and 34 deletions

View File

@@ -0,0 +1,29 @@
# Multi-item result preservation fix
## Objective
Preserve the terminal outcome of each item executed by the primary transactional tool. A failure in a later auxiliary/composite step must not erase successful item outcomes or turn an already-completed multi-item operation into a total failure.
## Framework-only change
No agent/template business logic is required for this fix. The runtime now recognizes an explicit structural contract on the requested tool: a `results` array with two or more entries carrying boolean `success` or `ok` fields.
For workflow-backed tools the authoritative source is preferentially:
`workflow.output[tool_name].results`
The framework derives one of:
- `SUCCESS`
- `PARTIAL_SUCCESS`
- `FAILED`
and publishes a generic `metadata.multi_item` summary with item counts.
When one or more primary items completed successfully but the outer wrapper is `ok=false` because a later auxiliary/post-processing step failed, the normalized result preserves the original failure as `original_ok=false` / `secondary_error` while exposing the requested operation as operationally successful (`ok=true`).
The LLM context also receives an explicit generic instruction that successful per-item terminal evidence cannot be overwritten by a secondary error and that mixed outcomes must be reported individually.
## Regression tests
Added `tests/test_multi_item_result_normalization.py` covering:
- 2 success + 1 failure -> `PARTIAL_SUCCESS`;
- all primary items successful + later secondary failure -> primary `SUCCESS` preserved;
- single-item behavior unchanged;
- unrelated nested tool results cannot override failure of the requested primary tool.

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -9,11 +9,18 @@ profiles:
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 700 max_tokens: 700
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -9,11 +9,18 @@ profiles:
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 700 max_tokens: 700
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -0,0 +1,58 @@
# Correção — `max_tokens` por `llm_profiles.yaml` / `.env`
## Objetivo
Remover `max_tokens` efetivos fixados nos pontos de inferência do framework sem perder compatibilidade com os budgets anteriores.
A precedência usada pelos pontos corrigidos passa a ser:
1. `max_tokens` do profile selecionado em `llm_profiles.yaml`;
2. `max_tokens` do profile `default` do mesmo YAML, quando aplicável ao resolver;
3. `LLM_MAX_TOKENS` configurado no ambiente / `.env`;
4. valor legado do ponto de inferência, usado somente como fallback de compatibilidade.
O parâmetro interno `fallback_max_tokens` **não é uma nova configuração do usuário**. Ele apenas preserva o valor histórico quando não existe configuração externa.
## Pontos corrigidos
- Enterprise Router:
- transaction parameter relevance: fallback legado `256`;
- transaction intent shift: fallback legado `512`;
- multi-intent: fallback legado `768`;
- routing normal: fallback legado `512`.
- MCP parameter extraction: fallback `80`.
- Processing interruption classifier: fallback `8`.
- LLM guardrail: fallback `600`.
- RAG query rewrite: fallback `300`.
- RAG context compression: mantém a fórmula histórica `max(512, max_chars // 3)` apenas como fallback.
- Conversation summary memory: mantém a fórmula histórica `max(256, max_summary_chars // 4)` apenas como fallback.
## Atualização de `llm_profiles.yaml`
Como os quatro pontos do Enterprise Router usam o mesmo profile `router`, o profile foi ajustado para o **maior budget histórico**, conforme solicitado:
```yaml
router:
max_tokens: 768
```
Também foram garantidos os profiles explícitos:
```yaml
processing_interruption_classifier:
max_tokens: 8
mcp_parameter_extraction:
max_tokens: 80
```
Os profiles já existentes foram mantidos:
- `guardrail: 600`
- `rag_rewriter: 300`
- `rag_compressor: 1200`
- `summary_memory: 1200`
## Compatibilidade
Se `llm_profiles.yaml` não estiver disponível e `LLM_MAX_TOKENS` não estiver definido, cada ponto corrigido continua usando seu budget histórico. Portanto a correção não transforma a ausência de configuração em `2048` apenas por causa do default global de `Settings`.

View File

@@ -9,3 +9,11 @@ profiles:
model: mock-judge model: mock-judge
temperature: 0 temperature: 0
max_tokens: 300 max_tokens: 300
mcp_parameter_extraction:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 80
timeout_seconds: 5

View File

@@ -58,7 +58,7 @@ async def classify_processing_interruption(
response = await llm.ainvoke( response = await llm.ainvoke(
[{"role": "system", "content": prompt}], [{"role": "system", "content": prompt}],
temperature=0, temperature=0,
max_tokens=8, fallback_max_tokens=8,
profile_name=profile_name, profile_name=profile_name,
component_name=profile_name, component_name=profile_name,
generation_name=f"llm.{profile_name}", generation_name=f"llm.{profile_name}",

View File

@@ -47,7 +47,7 @@ class LLMGuardrailRail(Guardrail):
{"role": "user", "content": prompt}, {"role": "user", "content": prompt},
], ],
temperature=0, temperature=0,
max_tokens=600, fallback_max_tokens=600,
profile_name=self.profile_name, profile_name=self.profile_name,
component_name=self.profile_name, component_name=self.profile_name,
generation_name=f"llm.{self.profile_name}", generation_name=f"llm.{self.profile_name}",

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import copy import copy
import logging import logging
import os
import re import re
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -164,6 +165,66 @@ class LLMProfileResolver:
return effective return effective
def resolve_configured_value(
self,
profile_name: str | None,
key: str,
*,
env_var: str | None = None,
fallback: Any = None,
) -> Any:
"""Resolve one configuration value with an explicit compatibility fallback.
Precedence is intentionally aligned with the framework configuration model:
1. specific ``llm_profiles.yaml`` profile;
2. YAML ``default`` profile;
3. explicitly configured general environment variable;
4. the caller-provided legacy fallback.
The last step is important for old inference points that historically had a
local constant. Pydantic's Settings object always has framework defaults, so
checking only ``settings.LLM_MAX_TOKENS`` would make an *unset* environment
indistinguishable from the global default.
"""
selected_name = self.normalize_profile_name(profile_name)
if self.enabled:
specific_profile = self._profiles.get(selected_name) or {}
default_profile = self._profiles.get("default") or {}
if key in specific_profile and specific_profile.get(key) is not None:
return specific_profile.get(key)
if key in default_profile and default_profile.get(key) is not None:
return default_profile.get(key)
if env_var:
raw = os.getenv(env_var)
if raw is not None and str(raw).strip() != "":
raw = str(raw).strip()
try:
if isinstance(fallback, bool):
return raw.lower() in {"1", "true", "yes", "on"}
if isinstance(fallback, int) and not isinstance(fallback, bool):
return int(raw)
if isinstance(fallback, float):
return float(raw)
except (TypeError, ValueError):
logger.warning(
"Invalid %s=%r for profile=%s key=%s; using legacy fallback=%r",
env_var, raw, selected_name, key, fallback,
)
return fallback
return raw
# Also honor a value explicitly supplied when Settings was instantiated
# programmatically, without treating the class default as configuration.
field_name = env_var or ""
fields_set = getattr(self.settings, "model_fields_set", set()) or set()
if field_name and field_name in fields_set:
value = getattr(self.settings, field_name, None)
if value is not None:
return value
return fallback
def normalize_profile_name(self, profile_name: str | None) -> str: def normalize_profile_name(self, profile_name: str | None) -> str:
return _canonical_profile_name(profile_name) return _canonical_profile_name(profile_name)

View File

@@ -370,6 +370,14 @@ class OCICompatibleOpenAIProvider(LLMProvider):
profile_name = kwargs.pop("profile_name", None) profile_name = kwargs.pop("profile_name", None)
component_name = kwargs.pop("component_name", None) or kwargs.pop("component", None) or profile_name or "default" component_name = kwargs.pop("component_name", None) or kwargs.pop("component", None) or profile_name or "default"
generation_name = kwargs.pop("generation_name", None) or f"llm.{component_name}" generation_name = kwargs.pop("generation_name", None) or f"llm.{component_name}"
fallback_max_tokens = kwargs.pop("fallback_max_tokens", None)
if fallback_max_tokens is not None:
kwargs["max_tokens"] = self.profile_resolver.resolve_configured_value(
profile_name,
"max_tokens",
env_var="LLM_MAX_TOKENS",
fallback=fallback_max_tokens,
)
generation_name, generation_mapping_meta = _normalize_generation_name(self.telemetry, generation_name) generation_name, generation_mapping_meta = _normalize_generation_name(self.telemetry, generation_name)
effective = self.profile_resolver.resolve(profile_name, **kwargs) effective = self.profile_resolver.resolve(profile_name, **kwargs)
provider = str(effective.get("provider") or self.provider_name) provider = str(effective.get("provider") or self.provider_name)

View File

@@ -167,7 +167,7 @@ class ConversationSummaryMemory:
summary = await self.llm.ainvoke([ summary = await self.llm.ainvoke([
{"role": "system", "content": "Você resume memória conversacional para agentes corporativos."}, {"role": "system", "content": "Você resume memória conversacional para agentes corporativos."},
{"role": "user", "content": prompt}, {"role": "user", "content": prompt},
], max_tokens=max(256, max_summary_chars // 4), temperature=0.1, profile_name="summary_memory", component_name="summary_memory", generation_name="llm.summary_memory") ], fallback_max_tokens=max(256, max_summary_chars // 4), temperature=0.1, profile_name="summary_memory", component_name="summary_memory", generation_name="llm.summary_memory")
summary = (summary or "").strip() summary = (summary or "").strip()
if not summary: if not summary:
return self._deterministic_summary(previous_summary=previous_summary, messages=messages, max_chars=max_summary_chars) return self._deterministic_summary(previous_summary=previous_summary, messages=messages, max_chars=max_summary_chars)

View File

@@ -84,7 +84,7 @@ class RagService:
{'role': 'user', 'content': prompt}, {'role': 'user', 'content': prompt},
], ],
temperature=0, temperature=0,
max_tokens=300, fallback_max_tokens=300,
profile_name=profile_name, profile_name=profile_name,
component_name=profile_name, component_name=profile_name,
generation_name=f"llm.{profile_name}", generation_name=f"llm.{profile_name}",
@@ -112,7 +112,7 @@ class RagService:
{'role': 'user', 'content': prompt}, {'role': 'user', 'content': prompt},
], ],
temperature=0, temperature=0,
max_tokens=max(512, max_chars // 3), fallback_max_tokens=max(512, max_chars // 3),
profile_name=profile_name, profile_name=profile_name,
component_name=profile_name, component_name=profile_name,
generation_name=f"llm.{profile_name}", generation_name=f"llm.{profile_name}",

View File

@@ -1045,7 +1045,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(payload, ensure_ascii=False)}, {"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
max_tokens=256, fallback_max_tokens=256,
profile_name="router", profile_name="router",
component_name="router.transaction_parameter_relevance", component_name="router.transaction_parameter_relevance",
generation_name="llm.transaction_parameter_relevance", generation_name="llm.transaction_parameter_relevance",
@@ -1328,7 +1328,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(user, ensure_ascii=False)}, {"role": "user", "content": json.dumps(user, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
max_tokens=512, fallback_max_tokens=512,
profile_name="router", profile_name="router",
component_name="router", component_name="router",
generation_name="llm.transaction_intent_shift", generation_name="llm.transaction_intent_shift",
@@ -1674,7 +1674,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(payload, ensure_ascii=False)}, {"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
max_tokens=768, fallback_max_tokens=768,
profile_name="router", profile_name="router",
component_name="router.multi_intent", component_name="router.multi_intent",
generation_name="llm.router.multi_intent", generation_name="llm.router.multi_intent",
@@ -1730,7 +1730,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(user, ensure_ascii=False)}, {"role": "user", "content": json.dumps(user, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
max_tokens=512, fallback_max_tokens=512,
profile_name="router", profile_name="router",
component_name="router", component_name="router",
generation_name="llm.router", generation_name="llm.router",

View File

@@ -849,7 +849,7 @@ class AgentRuntimeMixin:
component_name="mcp_parameter_extraction", component_name="mcp_parameter_extraction",
generation_name="llm.mcp_parameter_extraction", generation_name="llm.mcp_parameter_extraction",
temperature=0.0, temperature=0.0,
max_tokens=80, fallback_max_tokens=80,
) )
raw = self._llm_response_text(response).strip() raw = self._llm_response_text(response).strip()
payload = parse_json_object(raw) payload = parse_json_object(raw)

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
@@ -72,6 +78,13 @@ profiles:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0.2 temperature: 0.2
mcp_parameter_extraction:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 80
timeout_seconds: 5
transaction_parameter_extraction: transaction_parameter_extraction:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1-mini model: openai.gpt-4.1-mini

View File

@@ -15,11 +15,18 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 700 max_tokens: 700
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
# Safety / evaluation # Safety / evaluation
guardrail: guardrail:
@@ -87,3 +94,11 @@ profiles:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0.2 temperature: 0.2
mcp_parameter_extraction:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 80
timeout_seconds: 5

View File

@@ -58,7 +58,7 @@ async def classify_processing_interruption(
response = await llm.ainvoke( response = await llm.ainvoke(
[{"role": "system", "content": prompt}], [{"role": "system", "content": prompt}],
temperature=0, temperature=0,
max_tokens=8, fallback_max_tokens=8,
profile_name=profile_name, profile_name=profile_name,
component_name=profile_name, component_name=profile_name,
generation_name=f"llm.{profile_name}", generation_name=f"llm.{profile_name}",

View File

@@ -49,7 +49,7 @@ class LLMGuardrailRail(Guardrail):
{"role": "user", "content": prompt}, {"role": "user", "content": prompt},
], ],
temperature=0, temperature=0,
max_tokens=600, fallback_max_tokens=600,
profile_name=self.profile_name, profile_name=self.profile_name,
component_name=self.profile_name, component_name=self.profile_name,
generation_name=f"llm.{self.profile_name}", generation_name=f"llm.{self.profile_name}",

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import copy import copy
import logging import logging
import os
import re import re
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -164,6 +165,66 @@ class LLMProfileResolver:
return effective return effective
def resolve_configured_value(
self,
profile_name: str | None,
key: str,
*,
env_var: str | None = None,
fallback: Any = None,
) -> Any:
"""Resolve one configuration value with an explicit compatibility fallback.
Precedence is intentionally aligned with the framework configuration model:
1. specific ``llm_profiles.yaml`` profile;
2. YAML ``default`` profile;
3. explicitly configured general environment variable;
4. the caller-provided legacy fallback.
The last step is important for old inference points that historically had a
local constant. Pydantic's Settings object always has framework defaults, so
checking only ``settings.LLM_MAX_TOKENS`` would make an *unset* environment
indistinguishable from the global default.
"""
selected_name = self.normalize_profile_name(profile_name)
if self.enabled:
specific_profile = self._profiles.get(selected_name) or {}
default_profile = self._profiles.get("default") or {}
if key in specific_profile and specific_profile.get(key) is not None:
return specific_profile.get(key)
if key in default_profile and default_profile.get(key) is not None:
return default_profile.get(key)
if env_var:
raw = os.getenv(env_var)
if raw is not None and str(raw).strip() != "":
raw = str(raw).strip()
try:
if isinstance(fallback, bool):
return raw.lower() in {"1", "true", "yes", "on"}
if isinstance(fallback, int) and not isinstance(fallback, bool):
return int(raw)
if isinstance(fallback, float):
return float(raw)
except (TypeError, ValueError):
logger.warning(
"Invalid %s=%r for profile=%s key=%s; using legacy fallback=%r",
env_var, raw, selected_name, key, fallback,
)
return fallback
return raw
# Also honor a value explicitly supplied when Settings was instantiated
# programmatically, without treating the class default as configuration.
field_name = env_var or ""
fields_set = getattr(self.settings, "model_fields_set", set()) or set()
if field_name and field_name in fields_set:
value = getattr(self.settings, field_name, None)
if value is not None:
return value
return fallback
def normalize_profile_name(self, profile_name: str | None) -> str: def normalize_profile_name(self, profile_name: str | None) -> str:
return _canonical_profile_name(profile_name) return _canonical_profile_name(profile_name)

View File

@@ -370,6 +370,14 @@ class OCICompatibleOpenAIProvider(LLMProvider):
profile_name = kwargs.pop("profile_name", None) profile_name = kwargs.pop("profile_name", None)
component_name = kwargs.pop("component_name", None) or kwargs.pop("component", None) or profile_name or "default" component_name = kwargs.pop("component_name", None) or kwargs.pop("component", None) or profile_name or "default"
generation_name = kwargs.pop("generation_name", None) or f"llm.{component_name}" generation_name = kwargs.pop("generation_name", None) or f"llm.{component_name}"
fallback_max_tokens = kwargs.pop("fallback_max_tokens", None)
if fallback_max_tokens is not None:
kwargs["max_tokens"] = self.profile_resolver.resolve_configured_value(
profile_name,
"max_tokens",
env_var="LLM_MAX_TOKENS",
fallback=fallback_max_tokens,
)
generation_name, generation_mapping_meta = _normalize_generation_name(self.telemetry, generation_name) generation_name, generation_mapping_meta = _normalize_generation_name(self.telemetry, generation_name)
effective = self.profile_resolver.resolve(profile_name, **kwargs) effective = self.profile_resolver.resolve(profile_name, **kwargs)
provider = str(effective.get("provider") or self.provider_name) provider = str(effective.get("provider") or self.provider_name)

View File

@@ -167,7 +167,7 @@ class ConversationSummaryMemory:
summary = await self.llm.ainvoke([ summary = await self.llm.ainvoke([
{"role": "system", "content": "Você resume memória conversacional para agentes corporativos."}, {"role": "system", "content": "Você resume memória conversacional para agentes corporativos."},
{"role": "user", "content": prompt}, {"role": "user", "content": prompt},
], max_tokens=max(256, max_summary_chars // 4), temperature=0.1, profile_name="summary_memory", component_name="summary_memory", generation_name="llm.summary_memory") ], fallback_max_tokens=max(256, max_summary_chars // 4), temperature=0.1, profile_name="summary_memory", component_name="summary_memory", generation_name="llm.summary_memory")
summary = (summary or "").strip() summary = (summary or "").strip()
if not summary: if not summary:
return self._deterministic_summary(previous_summary=previous_summary, messages=messages, max_chars=max_summary_chars) return self._deterministic_summary(previous_summary=previous_summary, messages=messages, max_chars=max_summary_chars)

View File

@@ -98,7 +98,7 @@ class RagService:
{'role': 'user', 'content': prompt}, {'role': 'user', 'content': prompt},
], ],
temperature=0, temperature=0,
max_tokens=300, fallback_max_tokens=300,
profile_name=profile_name, profile_name=profile_name,
component_name=profile_name, component_name=profile_name,
generation_name=f"llm.{profile_name}", generation_name=f"llm.{profile_name}",
@@ -126,7 +126,7 @@ class RagService:
{'role': 'user', 'content': prompt}, {'role': 'user', 'content': prompt},
], ],
temperature=0, temperature=0,
max_tokens=max(512, max_chars // 3), fallback_max_tokens=max(512, max_chars // 3),
profile_name=profile_name, profile_name=profile_name,
component_name=profile_name, component_name=profile_name,
generation_name=f"llm.{profile_name}", generation_name=f"llm.{profile_name}",

View File

@@ -1045,7 +1045,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(payload, ensure_ascii=False)}, {"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
max_tokens=256, fallback_max_tokens=256,
profile_name="router", profile_name="router",
component_name="router.transaction_parameter_relevance", component_name="router.transaction_parameter_relevance",
generation_name="llm.transaction_parameter_relevance", generation_name="llm.transaction_parameter_relevance",
@@ -1328,7 +1328,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(user, ensure_ascii=False)}, {"role": "user", "content": json.dumps(user, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
max_tokens=512, fallback_max_tokens=512,
profile_name="router", profile_name="router",
component_name="router", component_name="router",
generation_name="llm.transaction_intent_shift", generation_name="llm.transaction_intent_shift",
@@ -1674,7 +1674,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(payload, ensure_ascii=False)}, {"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
max_tokens=768, fallback_max_tokens=768,
profile_name="router", profile_name="router",
component_name="router.multi_intent", component_name="router.multi_intent",
generation_name="llm.router.multi_intent", generation_name="llm.router.multi_intent",
@@ -1730,7 +1730,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(user, ensure_ascii=False)}, {"role": "user", "content": json.dumps(user, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
max_tokens=512, fallback_max_tokens=512,
profile_name="router", profile_name="router",
component_name="router", component_name="router",
generation_name="llm.router", generation_name="llm.router",

View File

@@ -849,7 +849,7 @@ class AgentRuntimeMixin:
component_name="mcp_parameter_extraction", component_name="mcp_parameter_extraction",
generation_name="llm.mcp_parameter_extraction", generation_name="llm.mcp_parameter_extraction",
temperature=0.0, temperature=0.0,
max_tokens=80, fallback_max_tokens=80,
) )
raw = self._llm_response_text(response).strip() raw = self._llm_response_text(response).strip()
payload = parse_json_object(raw) payload = parse_json_object(raw)

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,11 +15,17 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 500 max_tokens: 768
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -0,0 +1,95 @@
from __future__ import annotations
import os
import sys
from pathlib import Path
from types import SimpleNamespace
import yaml
ROOT = Path(__file__).resolve().parents[1]
LIB = ROOT / "libs" / "agent_framework" / "src"
if str(LIB) not in sys.path:
sys.path.insert(0, str(LIB))
from agent_framework.llm.profile_resolver import LLMProfileResolver
def _settings(path: Path):
return SimpleNamespace(
LLM_PROFILES_PATH=str(path),
LLM_PROVIDER="oci_openai",
OCI_GENAI_MODEL="openai.gpt-4.1",
LLM_TEMPERATURE=0.2,
LLM_MAX_TOKENS=2048,
LLM_TIMEOUT_SECONDS=120,
OCI_GENAI_BASE_URL="",
OCI_GENAI_API_KEY=None,
OCI_GENAI_PROJECT_OCID=None,
OCI_AUTH_MODE="config_file",
OCI_GENAI_ENDPOINT=None,
OCI_REGION="",
model_fields_set=set(),
)
def test_profile_max_tokens_wins_over_env_and_legacy_fallback(tmp_path, monkeypatch):
profile = tmp_path / "llm_profiles.yaml"
profile.write_text(
"profiles:\n"
" default:\n"
" provider: oci_openai\n"
" router:\n"
" max_tokens: 768\n",
encoding="utf-8",
)
monkeypatch.setenv("LLM_MAX_TOKENS", "999")
resolver = LLMProfileResolver.from_settings(_settings(profile))
assert resolver.resolve_configured_value(
"router", "max_tokens", env_var="LLM_MAX_TOKENS", fallback=512
) == 768
def test_env_max_tokens_wins_when_profiles_file_is_absent(tmp_path, monkeypatch):
missing = tmp_path / "missing.yaml"
monkeypatch.setenv("LLM_MAX_TOKENS", "901")
resolver = LLMProfileResolver.from_settings(_settings(missing))
assert resolver.resolve_configured_value(
"router", "max_tokens", env_var="LLM_MAX_TOKENS", fallback=512
) == 901
def test_legacy_fallback_is_kept_when_yaml_and_env_are_absent(tmp_path, monkeypatch):
missing = tmp_path / "missing.yaml"
monkeypatch.delenv("LLM_MAX_TOKENS", raising=False)
resolver = LLMProfileResolver.from_settings(_settings(missing))
assert resolver.resolve_configured_value(
"router", "max_tokens", env_var="LLM_MAX_TOKENS", fallback=512
) == 512
def test_canonical_profiles_keep_existing_budgets_and_router_uses_largest_value():
profile_path = ROOT / "libs" / "agent_framework" / "config" / "llm_profiles.yaml"
profiles = yaml.safe_load(profile_path.read_text(encoding="utf-8"))["profiles"]
assert profiles["router"]["max_tokens"] == 768
assert profiles["processing_interruption_classifier"]["max_tokens"] == 8
assert profiles["mcp_parameter_extraction"]["max_tokens"] == 80
assert profiles["guardrail"]["max_tokens"] == 600
assert profiles["rag_rewriter"]["max_tokens"] == 300
assert profiles["rag_compressor"]["max_tokens"] == 1200
assert profiles["summary_memory"]["max_tokens"] == 1200
def test_inference_points_use_fallback_max_tokens_instead_of_direct_max_tokens():
checks = {
"libs/agent_framework/src/agent_framework/routing/enterprise_router.py": [256, 512, 768],
"libs/agent_framework/src/agent_framework/runtime/agent_runtime.py": [80],
"libs/agent_framework/src/agent_framework/channels/interruption.py": [8],
"libs/agent_framework/src/agent_framework/guardrails/llm_rails.py": [600],
"libs/agent_framework/src/agent_framework/rag/rag_service.py": [300],
}
for relative, values in checks.items():
text = (ROOT / relative).read_text(encoding="utf-8")
for value in values:
assert f"fallback_max_tokens={value}" in text
assert f"max_tokens={value}," not in text.replace(f"fallback_max_tokens={value},", "")