fix(codex-bridge): flatten top-level oneOf/anyOf/allOf in tool schemas for external models

Since bridge 7 external models get Codex's classic tools. One desktop tool has a top-level union schema,
which Claude rejects ("input_schema does not support oneOf, allOf, or anyOf at the top level"), so every
Opus request from the desktop app failed with a bare "upstream 400". The earlier context-size diagnosis
was wrong: the request still failed after compaction (239 KB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
yilsemandClaude Opus 5.5 committed 2026-10-11 01:08:27 +03:00
1 parent 1445e204da
commit 1e2b8e2b9c
4 files changed
+68 -3

No files matched your search

@@ -281,6 +281,48 @@ SLIM_CLONES = bool(CFG.get("slim_clones", False))
SLIM_FIELDS = CFG.get("slim_fields", ["model_messages", "base_instructions"])
# ---- Codex'in yeni arac bicimini duz 'tools' listesine acma (9Router gibi ceviricilerin anlamasi icin) ----
def sema_duzelt(sema):
"""Arac parametre semasinin EN UST duzeyindeki oneOf/anyOf/allOf'u tek nesne semasina indirir.
Claude bunu reddediyor ("input_schema does not support oneOf, allOf, or anyOf at the top level"; olcum
11 Ekim 2026: masaustu Codex'in klasik arac listesindeki bir arac yuzunden her Opus istegi 400 aldi).
Secenek nesnelerinin alanlari birlestirilir; zorunlu alanlar oneOf/anyOf'ta kesisim, allOf'ta birlesimdir.
Donus: (sema, degisti mi)."""
if not isinstance(sema, dict) or not any(k in sema for k in ("oneOf", "anyOf", "allOf")):
return sema, False
yeni = {k: v for k, v in sema.items() if k not in ("oneOf", "anyOf", "allOf")}
alanlar = dict(yeni.get("properties") or {})
zorunlu = set(yeni.get("required") or [])
for anahtar in ("oneOf", "anyOf", "allOf"):
secenekler = [x for x in (sema.get(anahtar) or []) if isinstance(x, dict)]
kesisim = None
for secenek in secenekler:
secenek, _ = sema_duzelt(secenek)
for ad, tanim in (secenek.get("properties") or {}).items():
alanlar.setdefault(ad, tanim)
gerekli = set(secenek.get("required") or [])
if anahtar == "allOf":
zorunlu |= gerekli
else:
kesisim = gerekli if kesisim is None else (kesisim & gerekli)
if anahtar != "allOf" and kesisim:
zorunlu |= kesisim
yeni["type"] = "object"
yeni["properties"] = alanlar
if zorunlu:
yeni["required"] = sorted(a for a in zorunlu if a in alanlar)
else:
yeni.pop("required", None)
return yeni, True
def arac_semalarini_duzelt(req):
"""Dis modele giden duz arac listesinde ust duzey birlesimli semalari duzeltir. Donus: duzeltilen arac sayisi."""
n = 0
for t in req.get("tools") or []:
if isinstance(t, dict) and isinstance(t.get("parameters"), dict):
t["parameters"], degisti = sema_duzelt(t["parameters"])
n += bool(degisti)
return n
def flatten_request(req):
"""additional_tools/namespace/custom -> duz function tools. Donus: custom arac adlari kumesi."""
custom = set()
@@ -1238,6 +1280,9 @@ class H(http.server.BaseHTTPRequestHandler):
rewriter = None
if flat if flat is not None else route.get("flatten_tools"):
custom = flatten_request(req)
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)
fix_tool_ids(req)
if via_router and CFG.get('resim_sadelestir', True):
@@ -31,7 +31,7 @@ class AccessPrograms(unittest.TestCase):
"fix_tool_ids": lambda req: 0,
"drop_foreign_items": lambda req: 0,
"SseRewriter": Mock(),
"log": lambda message: None, "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "is_hatti": __import__("is_hatti"), "GORULEN_ISTEMCI": set(), "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [],
"log": lambda message: None, "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "is_hatti": __import__("is_hatti"), "GORULEN_ISTEMCI": set(), "arac_semalarini_duzelt": lambda req: 0, "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [],
"DEBUG": False, "UA": "test", "RETRIES": 1, "UPSTREAM_IDLE_TIMEOUT": 3900,
}
exec(compile(ast.Module(body=[flatten, claude_sonu, responses], type_ignores=[]), "bridge.py", "exec"), self.env)
@@ -40,7 +40,7 @@ class ComboNotResent(unittest.TestCase):
"fix_tool_ids": lambda req: 0, "drop_foreign_items": lambda req: 0,
"SseRewriter": Mock(), "log": lambda message: None,
"DEBUG": False, "UA": "test", "RETRIES": 6, "UPSTREAM_IDLE_TIMEOUT": 3600,
"NO_RETRY_PATTERNS": [], "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "is_hatti": __import__("is_hatti"), "GORULEN_ISTEMCI": set(), "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [],
"NO_RETRY_PATTERNS": [], "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "is_hatti": __import__("is_hatti"), "GORULEN_ISTEMCI": set(), "arac_semalarini_duzelt": lambda req: 0, "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [],
}
exec(compile(ast.Module(body=[pick("flatten_request"), pick("claude_sonu_duzelt"), pick("is_timeout"), responses],
type_ignores=[]), "bridge.py", "exec"), self.env)
@@ -8,7 +8,8 @@ from pathlib import Path
import pencere
tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
names = {"fix_tool_ids", "SseRewriter", "phase_damgala", "asistan_mesaji_mi", "nokta_mesaji_mi", "hazir_yaniti"}
names = {"fix_tool_ids", "SseRewriter", "phase_damgala", "asistan_mesaji_mi", "nokta_mesaji_mi", "hazir_yaniti",
"sema_duzelt", "arac_semalarini_duzelt"}
nodes = [n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.name in names]
for node in tree.body:
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id in
@@ -60,6 +61,25 @@ class Ek(unittest.TestCase):
self.assertIn("çıkarıldı (köprü)", req["input"][1]["content"][0]["text"])
class Sema(unittest.TestCase):
def test_top_level_union_becomes_one_object_schema(self):
req = {"tools": [
{"type": "function", "name": "a", "parameters": {"oneOf": [
{"type": "object", "properties": {"x": {"type": "string"}, "k": {"type": "string"}}, "required": ["x", "k"]},
{"type": "object", "properties": {"y": {"type": "integer"}, "k": {"type": "string"}}, "required": ["y", "k"]}]}},
{"type": "function", "name": "b", "parameters": {"type": "object", "properties": {"z": {"anyOf": [{"type": "string"}, {"type": "null"}]}}}},
{"type": "function", "name": "c", "parameters": {"allOf": [
{"type": "object", "properties": {"p": {"type": "string"}}, "required": ["p"]},
{"type": "object", "properties": {"q": {"type": "string"}}, "required": ["q"]}]}}]}
self.assertEqual(env["arac_semalarini_duzelt"](req), 2)
a, b, c = (t["parameters"] for t in req["tools"])
self.assertEqual((a["type"], sorted(a["properties"]), a["required"]), ("object", ["k", "x", "y"], ["k"]))
self.assertNotIn("oneOf", a)
self.assertIn("anyOf", b["properties"]["z"]) # nested unions are left alone
self.assertEqual((sorted(c["properties"]), c["required"]), (["p", "q"], ["p", "q"]))
self.assertEqual(env["arac_semalarini_duzelt"](req), 0)
class Nokta(unittest.TestCase):
def user(self, content):
return {"type": "message", "role": "user", "content": content}