feat: add emulated tool-calling bridge for Lingma
Add a proxy-side tool emulation layer so Lingma requests can surface stable OpenAI tool_calls and Anthropic tool_use blocks even when upstream tool events are missing or inconsistent. Constraint: Keep native Lingma tool event bridging as the first path and layer emulation as a fallback Rejected: Depend exclusively on Lingma native tool/invoke events | tool visibility remains inconsistent across models and transports Confidence: high Scope-risk: moderate
This commit is contained in:
@@ -196,6 +196,33 @@ def _extract_tool_calls_from_text(text: str) -> list[dict[str, Any]] | None:
|
||||
return None
|
||||
|
||||
|
||||
def _extract_hash_tool_call_event_from_text(
|
||||
text: str,
|
||||
*,
|
||||
forced_tool_name: str | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
raw = (text or "").strip()
|
||||
if not raw:
|
||||
return None
|
||||
match = re.search(
|
||||
r"#Tool Call\s*```([A-Za-z0-9_\-.]+)\s*(\{.*?\})\s*```",
|
||||
raw,
|
||||
flags=re.S,
|
||||
)
|
||||
if not match:
|
||||
return None
|
||||
name = match.group(1).strip()
|
||||
if forced_tool_name and name != forced_tool_name:
|
||||
return None
|
||||
try:
|
||||
arguments = json.loads(match.group(2))
|
||||
except Exception:
|
||||
return None
|
||||
if not isinstance(arguments, dict):
|
||||
return None
|
||||
return {"name": name, "input": arguments}
|
||||
|
||||
|
||||
def _tool_code_single_arg_name(
|
||||
tools: list[dict[str, Any]] | None, forced_tool_name: str
|
||||
) -> str | None:
|
||||
@@ -342,6 +369,45 @@ def _forced_tool_fallback_event(
|
||||
)
|
||||
|
||||
|
||||
def _declared_tool_names(tools: list[dict[str, Any]] | None) -> list[str]:
|
||||
if not isinstance(tools, list):
|
||||
return []
|
||||
out: list[str] = []
|
||||
for tool in tools:
|
||||
name = _openai_tool_name(tool) or _anthropic_tool_name(tool)
|
||||
if name and name not in out:
|
||||
out.append(name)
|
||||
return out
|
||||
|
||||
|
||||
def _infer_tool_event_from_declared_tools(
|
||||
text: str,
|
||||
*,
|
||||
tools: list[dict[str, Any]] | None,
|
||||
) -> dict[str, Any] | None:
|
||||
for tool_name in _declared_tool_names(tools):
|
||||
inferred = _extract_function_call_event_from_text(
|
||||
text,
|
||||
forced_tool_name=tool_name,
|
||||
)
|
||||
if inferred is not None:
|
||||
return inferred
|
||||
inferred = _extract_hash_tool_call_event_from_text(
|
||||
text,
|
||||
forced_tool_name=tool_name,
|
||||
)
|
||||
if inferred is not None:
|
||||
return inferred
|
||||
inferred = _forced_tool_fallback_event(
|
||||
text,
|
||||
forced_tool_name=tool_name,
|
||||
tools=tools,
|
||||
)
|
||||
if inferred is not None:
|
||||
return inferred
|
||||
return None
|
||||
|
||||
|
||||
def _openai_tool_call(
|
||||
tool: dict[str, Any], *, forced_id: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
|
||||
Reference in New Issue
Block a user