fix(llm): unify JSON output handling across providers and fix tool+json_output compatibility

- Remove redundant `response_format` injection for VOLCANO provider since it's unsupported; rely on system prompt injection instead
- Extend system prompt JSON injection logic to cover VOLCANO and tool-enabled cases universally
- Simplify model parameter construction by removing redundant `params["model_kwargs"] = model_kwargs` assignments
- Refactor `CompatibleChatOpenAI._get_request_payload` to strip `response_format` when tools are present, avoiding strict validation errors in langchain_openai
- Fix timestamp calculation order in `datetime_tool.py` to avoid integer truncation before multiplication
This commit is contained in:
Timebomb2018
2026-04-17 14:19:40 +08:00
parent 52f7ea7456
commit 377ddd2b9b
5 changed files with 51 additions and 26 deletions

View File

@@ -101,12 +101,10 @@ class RedBearModelFactory:
extra_body["enable_thinking"] = True
if config.thinking_budget_tokens:
extra_body["thinking_budget"] = config.thinking_budget_tokens
params["extra_body"] = extra_body
# JSON 输出模式
if config.json_output:
model_kwargs = params.setdefault("model_kwargs", {})
model_kwargs["response_format"] = {"type": "json_object"}
params["model_kwargs"] = model_kwargs
return params
if provider in [ModelProvider.OPENAI, ModelProvider.XINFERENCE, ModelProvider.GPUSTACK, ModelProvider.OLLAMA, ModelProvider.VOLCANO]:
@@ -148,11 +146,12 @@ class RedBearModelFactory:
extra_body["enable_thinking"] = True
if config.thinking_budget_tokens:
extra_body["thinking_budget"] = config.thinking_budget_tokens
params["extra_body"] = extra_body
# JSON 输出模式
if config.json_output:
params.setdefault("model_kwargs", {})
params["model_kwargs"]["response_format"] = {"type": "json_object"}
model_kwargs = params.setdefault("model_kwargs", {})
# VOLCANO 模型不支持 response_formatJSON 输出由 system prompt 注入实现
if provider != ModelProvider.VOLCANO:
model_kwargs["response_format"] = {"type": "json_object"}
return params
elif provider == ModelProvider.DASHSCOPE:
params = {
@@ -172,11 +171,9 @@ class RedBearModelFactory:
model_kwargs["incremental_output"] = True
if config.thinking_budget_tokens:
model_kwargs["thinking_budget"] = config.thinking_budget_tokens
params["model_kwargs"] = model_kwargs
if config.json_output:
model_kwargs = params.setdefault("model_kwargs", {})
model_kwargs["response_format"] = {"type": "json_object"}
params["model_kwargs"] = model_kwargs
return params
elif provider == ModelProvider.BEDROCK:
# Bedrock 使用 AWS 凭证
@@ -225,8 +222,8 @@ class RedBearModelFactory:
}
# JSON 输出模式
if config.json_output:
params.setdefault("model_kwargs", {})
params["model_kwargs"]["response_format"] = {"type": "json_object"}
model_kwargs = params.setdefault("model_kwargs", {})
model_kwargs["response_format"] = {"type": "json_object"}
return params
else:
raise BusinessException(f"不支持的提供商: {provider}", code=BizCode.PROVIDER_NOT_SUPPORTED)
@@ -261,12 +258,13 @@ def get_provider_llm_class(config: RedBearModelConfig, type: ModelType = ModelTy
if provider == ModelProvider.VOLCANO:
return CompatibleChatOpenAI
if provider in [ModelProvider.OPENAI, ModelProvider.XINFERENCE, ModelProvider.GPUSTACK]:
if type == ModelType.LLM:
return OpenAI
elif type == ModelType.CHAT:
return ChatOpenAI
else:
raise BusinessException(f"不支持的模型提供商及类型: {provider}-{type}", code=BizCode.PROVIDER_NOT_SUPPORTED)
return CompatibleChatOpenAI
# if type == ModelType.LLM:
# return OpenAI
# elif type == ModelType.CHAT:
# return CompatibleChatOpenAI
# else:
# raise BusinessException(f"不支持的模型提供商及类型: {provider}-{type}", code=BizCode.PROVIDER_NOT_SUPPORTED)
elif provider == ModelProvider.DASHSCOPE:
return ChatTongyi
elif provider == ModelProvider.OLLAMA:

View File

@@ -8,12 +8,33 @@ from __future__ import annotations
from typing import Any, Optional, Union
from langchain_core.messages import BaseMessage
from langchain_core.outputs import ChatGenerationChunk, ChatResult
from langchain_openai import ChatOpenAI
class CompatibleChatOpenAI(ChatOpenAI):
"""火山和千问的omni兼容模型支持深度思考内容reasoning_content的流式和非流式透传。"""
"""火山和千问的omni兼容模型支持深度思考内容reasoning_content的流式和非流式透传。
同时修复 json_output + tools 同时使用时 langchain_openai 强制走 .parse()/.stream()
导致 strict 校验报错的问题:有工具时从 payload 中移除 response_format
让父类走普通 .create()/.astream() 路径JSON 输出由 system prompt 指令保证。
"""
def _get_request_payload(
self,
input_: list[BaseMessage],
*,
stop: list[str] | None = None,
**kwargs: Any,
) -> dict:
payload = super()._get_request_payload(input_, stop=stop, **kwargs)
# 有工具时 langchain_openai 检测到 response_format 会切换到 .parse()/.stream()
# 接口OpenAI SDK 要求此时所有工具必须 strict=True动态生成的工具不满足。
# 移除 response_format让父类走普通路径JSON 输出由 system prompt 指令保证。
if payload.get("tools") and "response_format" in payload:
payload.pop("response_format")
return payload
def _create_chat_result(self, response: Union[dict, Any], generation_info: Optional[dict] = None) -> ChatResult:
result = super()._create_chat_result(response, generation_info)