Preserve namespaced Codex tools across external model calls
Server CI and Windows package / verify (push) Successful in 2m11s
Server CI and Windows package / windows-package (push) Successful in 10m44s

This commit is contained in:
yilsem committed 2026-10-13 04:54:52 +03:00
1 parent a68d3e2272
commit 2cca6a66a6
7 files changed
+138 -15

No files matched your search

+62 -10
View File
@@ -40,7 +40,7 @@ from bekci_watch import MetadataFilter
# Installed only by owned native launcher, never an HTTP control endpoint.
WATCH_OWNER = None
SURUM = "13"
SURUM = "14"
HERE = os.path.dirname(os.path.abspath(__file__))
try:
with open(os.path.abspath(__file__), "rb") as _f:
@@ -363,15 +363,36 @@ def arac_semalarini_duzelt(req):
n += bool(degisti)
return n
def flatten_request(req):
def flatten_request(req, tool_names=None):
"""additional_tools/namespace/custom -> duz function tools. Donus: custom arac adlari kumesi."""
custom = set()
tools = []
def add_tool(t):
tool_names = tool_names if tool_names is not None else {}
identities = {}
wire_names = {}
def add_tool(t, namespace=None):
ty = t.get("type")
if ty in ("function", "custom"):
namespace = t.get("namespace") or namespace
name = t["name"]
if namespace:
import hashlib
identity = (namespace, name)
# A hash avoids collisions between underscore-containing namespace/name pairs.
alias = "ns_" + hashlib.sha256(json.dumps(identity).encode()).hexdigest()[:24] + "_" + re.sub(r"[^a-zA-Z0-9_-]", "_", name)[:32]
if alias in tool_names and tool_names[alias] != identity:
raise ValueError("Tool namespace alias collision")
tool_names[alias] = identity
identities[identity] = alias
t = dict(t, name=alias, description=f"Tool {namespace}.{name}. " + t.get("description", ""))
wire = t["name"]
identity = (namespace, name)
if wire in wire_names and wire_names[wire] != identity:
raise ValueError("Tool namespace alias collision")
wire_names[wire] = identity
if ty == "namespace":
for s in t.get("tools", []):
add_tool(s)
add_tool(s, ".".join(filter(None, (namespace, t.get("name")))))
elif ty == "function":
tools.append({"type": "function", "name": t["name"], "description": t.get("description", ""),
"parameters": t.get("parameters") or {"type": "object", "properties": {}}, "strict": False})
@@ -387,13 +408,34 @@ def flatten_request(req):
tools.append(t)
for t in req.get("tools", []) or []:
add_tool(t)
for it in req.get("input", []) or []:
if it.get("type") == "additional_tools":
for t in it.get("tools", []):
add_tool(t)
plain_names = {t.get("name") for t in tools if t.get("name") not in tool_names}
def flatten_name(item):
namespace, name = item.get("namespace"), item.get("name")
alias = identities.get((namespace, name))
if not namespace and name not in plain_names:
matches = [a for a, identity in tool_names.items() if identity[1] == name]
if len(matches) == 1:
alias = matches[0] # Legacy history recorded before namespaces were preserved.
if alias:
item = dict(item, name=alias)
item.pop("namespace", None)
return item
choice = req.get("tool_choice")
if isinstance(choice, dict):
req["tool_choice"] = flatten_name(choice)
if isinstance(choice.get("tools"), list):
req["tool_choice"] = dict(req["tool_choice"], tools=[flatten_name(t) for t in choice["tools"]])
new_input = []
for it in req.get("input", []) or []:
ty = it.get("type")
if ty == "additional_tools":
for t in it.get("tools", []):
add_tool(t)
continue
if ty in ("function_call", "custom_tool_call"):
it = flatten_name(it)
if ty in ("function_call_output", "custom_tool_call_output") and not it.get("call_id"):
# Desktop thread handoffs have no matching tool call. Preserve their
# content as imported context, not an invalid tool-role message.
@@ -527,9 +569,10 @@ class SseRewriter:
Ayrica dis modelin asistan mesajlarina phase damgasi koyar: mesajin "bitti" olayi, arkasindan ne
geldigi belli olana kadar bekletilir (arac cagrisi ya da baska oge -> commentary; yanit bitti ->
final_answer). Metin parcalari (delta) bekletilmez, canli akar."""
def __init__(self, custom, hedef=None, tasma=None, aracsiz=None, dolu=False):
def __init__(self, custom, hedef=None, tasma=None, aracsiz=None, dolu=False, tool_names=None):
self.dolu = dolu # istek hedef pencerenin buyuk kismini dolduruyor
self.custom = custom
self.tool_names = tool_names or {}
self.buf = b""
self.custom_ids = set()
self.bekleyen = None # phase karari bekleyen "output_item.done" olayi
@@ -634,6 +677,12 @@ class SseRewriter:
self.bekleyen = d
return once
return once + [d]
def _restore_tool_name(self, item):
if isinstance(item, dict) and item.get("type") in ("function_call", "custom_tool_call"):
identity = self.tool_names.get(item.get("name"))
if identity:
item["namespace"], item["name"] = identity
return item
def _fix(self, d):
t = d.get("type", "")
item = d.get("item")
@@ -655,6 +704,7 @@ class SseRewriter:
# Normalize before Codex persists the response. Request-only repair
# cannot help when the user later disables the bridge.
fix_tool_ids({"input": [d["item"]]})
self._restore_tool_name(d["item"])
return d
if t.startswith("response.function_call_arguments.") and d.get("item_id") in self.custom_ids:
return None # JSON parcalari custom input'a cevrilemez; done olayinda tam metin gidiyor
@@ -667,9 +717,10 @@ class SseRewriter:
item = {"type": "custom_tool_call", "id": item.get("id"), "call_id": item.get("call_id"),
"name": item.get("name"), "input": inp, "status": "completed"}
fix_tool_ids({"input": [item]})
out.append(item)
out.append(self._restore_tool_name(item))
d["response"]["output"] = out
fix_tool_ids({"input": out})
self._restore_tool_name(d.get("item"))
return d
def feed(self, chunk):
self.buf += chunk
@@ -1623,11 +1674,12 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"{tag} {cikan} resim not ile degistirildi ({kazanc // 1024} KB)")
rewriter = None
if flat if flat is not None else route.get("flatten_tools"):
custom = flatten_request(req)
tool_names = {}
custom = flatten_request(req, tool_names)
duzelen = arac_semalarini_duzelt(req)
if duzelen:
log(f"{tag} {duzelen} aracin semasi duzeltildi (ust duzey oneOf/anyOf/allOf)")
rewriter = SseRewriter(custom, hedef=target, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren)
rewriter = SseRewriter(custom, hedef=target, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren, tool_names=tool_names)
fix_tool_ids(req)
if route_name == catalog.get('route', '9router') and target.startswith('ag/gemini-'):
merged = coalesce_context(req)