From 1e2b8e2b9cf25686bc547b6aceb7f43f7fb3a6d2 Mon Sep 17 00:00:00 2001 From: yilsem Date: Sun, 11 Oct 2026 01:08:27 +0300 Subject: [PATCH] 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 --- apps/desktop/assets/codex-bridge/bridge.py | 45 +++++++++++++++++++ .../codex-bridge/test_access_programs.py | 2 +- .../assets/codex-bridge/test_combo_tekrar.py | 2 +- .../assets/codex-bridge/test_kopru7_ek.py | 22 ++++++++- 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/apps/desktop/assets/codex-bridge/bridge.py b/apps/desktop/assets/codex-bridge/bridge.py index 0d28542..106cc36 100644 --- a/apps/desktop/assets/codex-bridge/bridge.py +++ b/apps/desktop/assets/codex-bridge/bridge.py @@ -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): diff --git a/apps/desktop/assets/codex-bridge/test_access_programs.py b/apps/desktop/assets/codex-bridge/test_access_programs.py index 92d9901..5e6b8df 100644 --- a/apps/desktop/assets/codex-bridge/test_access_programs.py +++ b/apps/desktop/assets/codex-bridge/test_access_programs.py @@ -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) diff --git a/apps/desktop/assets/codex-bridge/test_combo_tekrar.py b/apps/desktop/assets/codex-bridge/test_combo_tekrar.py index 675a350..842ad71 100644 --- a/apps/desktop/assets/codex-bridge/test_combo_tekrar.py +++ b/apps/desktop/assets/codex-bridge/test_combo_tekrar.py @@ -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) diff --git a/apps/desktop/assets/codex-bridge/test_kopru7_ek.py b/apps/desktop/assets/codex-bridge/test_kopru7_ek.py index 0840b5b..87490a7 100644 --- a/apps/desktop/assets/codex-bridge/test_kopru7_ek.py +++ b/apps/desktop/assets/codex-bridge/test_kopru7_ek.py @@ -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}