Fix: support tool call config (#14616)

### What problem does this PR solve?
support tool call config

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
buua436
2026-05-07 15:54:57 +08:00
committed by GitHub
parent 5b162a0c46
commit 0501134820
9 changed files with 137 additions and 15 deletions

View File

@@ -277,10 +277,13 @@ class Agent(LLM, ToolBase):
return
if delta.find("**ERROR**") >= 0:
if self.get_exception_default_value():
self.set_output("content", self.get_exception_default_value())
yield self.get_exception_default_value()
fallback = self.get_exception_default_value()
self.set_output("content", fallback)
yield fallback
else:
self.set_output("_ERROR", delta)
self.set_output("content", delta)
yield delta
return
if not need2cite or cited:
yield delta

View File

@@ -29,6 +29,23 @@ from rag.utils.base64_image import test_image
from rag.llm import EmbeddingModel, ChatModel, RerankModel, CvModel, TTSModel, OcrModel, Seq2txtModel
def _resolve_my_llm_is_tools(o_dict: dict) -> bool:
decode_api_key_config = getattr(TenantLLMService, "_decode_api_key_config", None)
if callable(decode_api_key_config):
_, is_tools, _ = decode_api_key_config(o_dict.get("api_key", ""))
if is_tools is not None:
return bool(is_tools)
try:
base_name, fid = TenantLLMService.split_model_name_and_factory(o_dict["llm_name"])
llm_cfg = LLMService.query(llm_name=base_name, fid=fid) if fid else LLMService.query(llm_name=base_name)
if not llm_cfg and fid:
llm_cfg = LLMService.query(llm_name=base_name)
return bool(llm_cfg[0].is_tools) if llm_cfg else False
except Exception:
return False
@manager.route("/factories", methods=["GET"]) # noqa: F821
@login_required
def factories():
@@ -229,6 +246,19 @@ async def add_llm():
elif factory == "OpenDataLoader":
api_key = apikey_json(["api_key", "provider_order"])
existing_llm = None
existing_api_key = None
if req.get("api_key") is None:
existing_llms = TenantLLMService.query(tenant_id=current_user.id, llm_factory=factory, llm_name=llm_name)
if existing_llms:
existing_llm = existing_llms[0]
existing_api_key, _, existing_api_key_payload = TenantLLMService._decode_api_key_config(existing_llm.api_key)
if existing_api_key_payload is not None:
existing_api_key = existing_api_key_payload
if req.get("api_key") is None:
api_key = existing_api_key if existing_api_key is not None else "x"
llm = {
"tenant_id": current_user.id,
"llm_factory": factory,
@@ -353,6 +383,9 @@ async def add_llm():
if msg:
return get_data_error_result(message=msg)
if "is_tools" in req:
llm["api_key"] = TenantLLMService._encode_api_key_config(llm["api_key"], bool(req["is_tools"]))
if not TenantLLMService.filter_update([TenantLLM.tenant_id == current_user.id, TenantLLM.llm_factory == factory, TenantLLM.llm_name == llm["llm_name"]], llm):
TenantLLMService.save(**llm)
@@ -421,6 +454,7 @@ def my_llms():
"api_base": o_dict["api_base"] or "",
"max_tokens": o_dict["max_tokens"] or 8192,
"status": o_dict["status"] or "1",
"is_tools": _resolve_my_llm_is_tools(o_dict),
}
)
else:

View File

@@ -26,8 +26,14 @@ def get_model_config_by_id(tenant_model_id: int) -> dict:
if not found:
raise LookupError(f"Tenant Model with id {tenant_model_id} not found")
config_dict = model_config.to_dict()
api_key, is_tools, api_key_payload = TenantLLMService._decode_api_key_config(config_dict.get("api_key", ""))
config_dict["api_key"] = api_key
if api_key_payload is not None:
config_dict["api_key_payload"] = api_key_payload
if is_tools is not None:
config_dict["is_tools"] = is_tools
llm = LLMService.query(llm_name=config_dict["llm_name"])
if llm:
if "is_tools" not in config_dict and llm:
config_dict["is_tools"] = llm[0].is_tools
return config_dict
@@ -73,6 +79,12 @@ def get_model_config_by_type_and_name(tenant_id: str, model_type: str, model_nam
else:
# model_name without @factory
config_dict = model_config.to_dict()
api_key, is_tools, api_key_payload = TenantLLMService._decode_api_key_config(config_dict.get("api_key", ""))
config_dict["api_key"] = api_key
if api_key_payload is not None:
config_dict["api_key_payload"] = api_key_payload
if is_tools is not None:
config_dict["is_tools"] = is_tools
config_model_type = config_dict.get("model_type")
config_model_type = config_model_type.value if hasattr(config_model_type, "value") else config_model_type
if config_model_type != model_type_val and not (
@@ -83,7 +95,7 @@ def get_model_config_by_type_and_name(tenant_id: str, model_type: str, model_nam
f"Tenant Model with name {model_name} has type {config_model_type}, expected {model_type_val}"
)
llm = LLMService.query(llm_name=config_dict["llm_name"])
if llm:
if "is_tools" not in config_dict and llm:
config_dict["is_tools"] = llm[0].is_tools
return config_dict

View File

@@ -34,6 +34,42 @@ class LLMFactoriesService(CommonService):
class TenantLLMService(CommonService):
model = TenantLLM
@staticmethod
def _decode_api_key_config(raw_api_key: str) -> tuple[str, bool | None, str | None]:
if not raw_api_key:
return raw_api_key, None, None
try:
parsed = json.loads(raw_api_key)
except Exception:
return raw_api_key, None, None
if not isinstance(parsed, dict):
return raw_api_key, None, None
is_tools = bool(parsed["is_tools"]) if "is_tools" in parsed else None
if set(parsed.keys()) <= {"api_key", "is_tools"}:
return parsed.get("api_key", ""), is_tools, None
return parsed.get("api_key", raw_api_key), is_tools, raw_api_key
@staticmethod
def _encode_api_key_config(raw_api_key: str, is_tools: bool | None) -> str:
if is_tools is None:
return raw_api_key
try:
parsed = json.loads(raw_api_key or "{}")
except Exception:
parsed = None
if isinstance(parsed, dict):
payload = dict(parsed)
payload["is_tools"] = bool(is_tools)
return json.dumps(payload)
return json.dumps({"api_key": raw_api_key or "", "is_tools": bool(is_tools)})
@classmethod
@DB.connection_context()
def get_api_key(cls, tenant_id, model_name, model_type=None):
@@ -123,6 +159,12 @@ class TenantLLMService(CommonService):
model_config = cls.get_api_key(tenant_id, mdlnm, llm_type)
if model_config:
model_config = model_config.to_dict()
api_key, is_tools, api_key_payload = cls._decode_api_key_config(model_config.get("api_key", ""))
model_config["api_key"] = api_key
if api_key_payload is not None:
model_config["api_key_payload"] = api_key_payload
if is_tools is not None:
model_config["is_tools"] = is_tools
elif llm_type == LLMType.EMBEDDING and fid == "Builtin" and "tei-" in os.getenv("COMPOSE_PROFILES", "") and mdlnm == os.getenv("TEI_MODEL", ""):
embedding_cfg = settings.EMBEDDING_CFG
model_config = {"llm_factory": "Builtin", "api_key": embedding_cfg["api_key"], "llm_name": mdlnm, "api_base": embedding_cfg["base_url"]}
@@ -132,7 +174,7 @@ class TenantLLMService(CommonService):
llm = LLMService.query(llm_name=mdlnm) if not fid else LLMService.query(llm_name=mdlnm, fid=fid)
if not llm and fid: # for some cases seems fid mismatch
llm = LLMService.query(llm_name=mdlnm)
if llm:
if "is_tools" not in model_config and llm:
model_config["is_tools"] = llm[0].is_tools
return model_config
@@ -142,35 +184,36 @@ class TenantLLMService(CommonService):
if not model_config:
raise LookupError("Model config is required")
kwargs.update({"provider": model_config["llm_factory"]})
api_key = model_config.get("api_key_payload", model_config["api_key"])
if model_config["model_type"] == LLMType.EMBEDDING.value:
if model_config["llm_factory"] not in EmbeddingModel:
return None
return EmbeddingModel[model_config["llm_factory"]](model_config["api_key"], model_config["llm_name"], base_url=model_config["api_base"])
return EmbeddingModel[model_config["llm_factory"]](api_key, model_config["llm_name"], base_url=model_config["api_base"])
elif model_config["model_type"] == LLMType.RERANK:
if model_config["llm_factory"] not in RerankModel:
return None
return RerankModel[model_config["llm_factory"]](model_config["api_key"], model_config["llm_name"], base_url=model_config["api_base"])
return RerankModel[model_config["llm_factory"]](api_key, model_config["llm_name"], base_url=model_config["api_base"])
elif model_config["model_type"] == LLMType.IMAGE2TEXT.value:
if model_config["llm_factory"] not in CvModel:
return None
return CvModel[model_config["llm_factory"]](model_config["api_key"], model_config["llm_name"], lang, base_url=model_config["api_base"], **kwargs)
return CvModel[model_config["llm_factory"]](api_key, model_config["llm_name"], lang, base_url=model_config["api_base"], **kwargs)
elif model_config["model_type"] == LLMType.CHAT.value:
if model_config["llm_factory"] not in ChatModel:
return None
return ChatModel[model_config["llm_factory"]](model_config["api_key"], model_config["llm_name"], base_url=model_config["api_base"], **kwargs)
return ChatModel[model_config["llm_factory"]](api_key, model_config["llm_name"], base_url=model_config["api_base"], **kwargs)
elif model_config["model_type"] == LLMType.SPEECH2TEXT:
if model_config["llm_factory"] not in Seq2txtModel:
return None
return Seq2txtModel[model_config["llm_factory"]](key=model_config["api_key"], model_name=model_config["llm_name"], lang=lang, base_url=model_config["api_base"])
return Seq2txtModel[model_config["llm_factory"]](key=api_key, model_name=model_config["llm_name"], lang=lang, base_url=model_config["api_base"])
elif model_config["model_type"] == LLMType.TTS:
if model_config["llm_factory"] not in TTSModel:
return None
return TTSModel[model_config["llm_factory"]](
model_config["api_key"],
api_key,
model_config["llm_name"],
base_url=model_config["api_base"],
)
@@ -179,7 +222,7 @@ class TenantLLMService(CommonService):
if model_config["llm_factory"] not in OcrModel:
return None
return OcrModel[model_config["llm_factory"]](
key=model_config["api_key"],
key=api_key,
model_name=model_config["llm_name"],
base_url=model_config.get("api_base", ""),
**kwargs,

View File

@@ -5,6 +5,7 @@ export interface IAddLlmRequestBody {
api_base?: string; // chat|embedding|speech2text|image2text
api_key?: string | Record<string, any>;
max_tokens: number;
is_tools?: boolean;
}
export interface IDeleteLlmRequestBody {

View File

@@ -1126,6 +1126,9 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
Verify: 'Verify',
keyValid: 'Your API key is valid.',
keyInvalid: 'Your API key is invalid.',
enableToolCall: 'Enable tool call',
enableToolCallTip:
'Allow this model to call tools when the selected model type supports tool calling.',
deleteModel: 'Delete model',
bedrockCredentialsHint:
'Tip: Leave Access Key / Secret Key blank to use AWS IAM authentication.',

View File

@@ -1036,6 +1036,8 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
Verify: '验证',
keyValid: '你的 API 密钥有效。',
keyInvalid: '你的 API 密钥无效。',
enableToolCall: '启用工具调用',
enableToolCallTip: '当所选模型类型支持工具调用时,允许该模型调用工具。',
deleteModel: '删除模型',
modelEmptyTip: '暂无可用模型,<br>请先在右侧面板添加模型。',
sourceEmptyTip: '暂未添加任何数据源,请从下方选择一个进行连接。',

View File

@@ -228,6 +228,7 @@ export const useSubmitOllama = () => {
api_base: detailedData.api_base || '',
max_tokens: detailedData.max_tokens || 8192,
api_key: '',
is_tools: detailedData.is_tools || false,
};
setInitialValues(initialVals);
} else {

View File

@@ -115,6 +115,7 @@ const OllamaModal = ({
const getOptions = (factory: string) => {
return optionsMap[factory as LLMFactory] || optionsMap.Default;
};
const defaultToolCallEnabled = initialValues?.is_tools ?? false;
const baseFields: FormFieldConfig[] = [
{
@@ -177,6 +178,20 @@ const OllamaModal = ({
},
];
baseFields.push({
name: 'is_tools',
label: t('enableToolCall'),
type: FormFieldType.Switch,
required: false,
dependencies: ['model_type'],
shouldRender: (formValues: any) => {
const modelType = formValues?.model_type;
return modelType === 'chat' || modelType === 'image2text';
},
tooltip: t('enableToolCallTip'),
defaultValue: defaultToolCallEnabled,
});
// Add provider_order field only for OpenRouter
if (llmFactory === 'OpenRouter') {
baseFields.push({
@@ -214,14 +229,18 @@ const OllamaModal = ({
api_key: '',
vision: initialValues.model_type === 'image2text',
provider_order: initialValues.provider_order || '',
is_tools: initialValues.is_tools || false,
};
}
return {
model_type:
llmFactory in optionsMap
? optionsMap[llmFactory as LLMFactory]?.at(0)?.value
: 'embedding',
llmFactory === LLMFactory.Ollama || llmFactory === LLMFactory.VLLM
? 'chat'
: llmFactory in optionsMap
? optionsMap[llmFactory as LLMFactory]?.at(0)?.value
: 'embedding',
vision: false,
is_tools: false,
};
}, [editMode, initialValues, llmFactory]);
@@ -232,6 +251,7 @@ const OllamaModal = ({
values.model_type === 'chat' && values.vision
? 'image2text'
: values.model_type;
const supportsToolCall = modelType === 'chat' || modelType === 'image2text';
const data: IAddLlmRequestBody & { provider_order?: string } = {
llm_factory: llmFactory,
@@ -241,6 +261,9 @@ const OllamaModal = ({
api_key: values.api_key as string,
max_tokens: values.max_tokens as number,
};
if (supportsToolCall) {
data.is_tools = Boolean(values.is_tools);
}
// Add provider_order only if it exists (for OpenRouter)
if (values.provider_order) {