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:
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}
|
||||
|
||||
Reference in new issue
Block a user