Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db5e876f40 | ||
|
|
ab7a721fbc | ||
|
|
2cca6a66a6 |
No files matched your search
@@ -33,7 +33,7 @@ jobs:
|
||||
- name: Codex bridge regression tests
|
||||
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m unittest discover -s apps/desktop/assets/codex-bridge'
|
||||
- name: Native watch and speech mux pytest regressions
|
||||
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m pytest apps/desktop/assets/codex-bridge/test_bekci_watch.py apps/desktop/assets/codex-bridge/test_native_speech_mux.py -q -p no:cacheprovider -rf'
|
||||
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m pytest apps/desktop/assets/codex-bridge/test_bekci_watch.py apps/desktop/assets/codex-bridge/test_native_speech_mux.py apps/desktop/assets/codex-bridge/test_live_limits.py -q -p no:cacheprovider -rf'
|
||||
- name: Mobile and desktop startup regression tests
|
||||
run: |
|
||||
npm install --prefix "$RUNNER_TEMP/mobile-tests" --no-save --ignore-scripts --no-audit --no-fund --legacy-peer-deps vitest@4.1.10 ws@8.21.1
|
||||
|
||||
+5
-2
@@ -25,6 +25,8 @@ ayrıntı seviyesi, korunacak mesajlaşmalar, yüzde/token bütçesi ve otomatik
|
||||
seçimini sunar. Varsayılanlar: Gemini 3.8 Flash High, yedek Gemini 3 Flash,
|
||||
10 mesajlaşma, geçişte %70, otomatik tetiklemede %70 ve sonrasında %50.
|
||||
Kapasite bağlantıdan gelen model bilgisiyle hesaplanır; 272K genel tavan yoktur.
|
||||
`/models` içindeki doğrulanmış sayısal bağlam bilgisi, model adından çıkarılan
|
||||
yerel varsayılandan önce gelir; gözlenmiş daha düşük bir gerçek sınır korunur.
|
||||
Girdi hesabı talimatlar ve araçları da içeren temkinli bir token tahminidir.
|
||||
|
||||
Abonelik modellerinin kendi aralarındaki istekler ve GPT-Live ses yolu özgün
|
||||
@@ -58,8 +60,9 @@ npm run builder --workspace=apps/desktop -- --win nsis --x64 --publish never
|
||||
|
||||
Dağıtılacak derleme temiz, yayımlanmış Git commit'inden üretilmelidir. İlk açılış
|
||||
ajan kurulumunu paketteki `install-stamp.json` commit'ine sabitler. Windows
|
||||
imzası ve temiz kurulum doğrulanmadan bir paket doğrulanmış üretim sürümü diye
|
||||
sunulmamalıdır. macOS ve Linux paketleri bu beta kapsamında doğrulanmamıştır.
|
||||
paketleri imzasız EXE/ZIP olarak yayımlanır; SHA-256 ve gerçek Windows çalıştırma
|
||||
kontrolü gereklidir. Kullanıcı açıkça istemeden imzalama yapılmaz. macOS ve Linux
|
||||
paketleri bu beta kapsamında doğrulanmamıştır.
|
||||
|
||||
## Lisans ve kaynak
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import urllib.request
|
||||
import gecis_context
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SURUM = "13" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
|
||||
SURUM = "16" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
|
||||
try:
|
||||
with open(os.path.abspath(__file__), "rb") as _f:
|
||||
KOD = hashlib.sha256(_f.read()).hexdigest()[:8]
|
||||
|
||||
@@ -7,6 +7,7 @@ must not enter the budget preparation path.
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
|
||||
@@ -104,6 +105,31 @@ def portable_reasoning(req):
|
||||
return out
|
||||
result = []
|
||||
for item in items:
|
||||
if isinstance(item, dict) and item.get("type") == "agent_message":
|
||||
# Peer messages carry delegated tasks/results. External Responses
|
||||
# translators do not understand this Codex-specific item type.
|
||||
# Keep provenance as data, never as system/developer authority.
|
||||
content = item.get("content") or []
|
||||
if isinstance(content, str):
|
||||
content = [{"type": "input_text", "text": content}]
|
||||
portable = []
|
||||
for part in content:
|
||||
if isinstance(part, dict) and part.get("type") == "encrypted_content":
|
||||
# Codex peer delivery can put the literal task/result in
|
||||
# this wrapper. It is distinct from encrypted reasoning.
|
||||
# External translators otherwise silently drop the task.
|
||||
text = part.get("encrypted_content")
|
||||
if (not isinstance(text, str) or text.startswith("gAAAA")
|
||||
or re.fullmatch(r"[A-Za-z0-9_+/=-]{128,}", text)):
|
||||
raise TransferError("Alt ajan iletisi şifreli veya okunamıyor; görev eksik gönderilmedi.")
|
||||
portable.append({"type": "input_text", "text": text})
|
||||
else:
|
||||
portable.append(part)
|
||||
result.append({"type": "message", "role": "user", "content": [
|
||||
{"type": "input_text", "text": "Agent message: " + encoded({
|
||||
"author": item.get("author"), "recipient": item.get("recipient")})}
|
||||
] + portable})
|
||||
continue
|
||||
if not isinstance(item, dict) or item.get("type") != "reasoning":
|
||||
result.append(item)
|
||||
continue
|
||||
|
||||
@@ -348,11 +348,13 @@ def yukle(ua="AstraBridge/1.0 (codex)", timeout=30, home=None, ag=True):
|
||||
if not girdiler:
|
||||
raise ValueError("Gecitte kullanilabilir OmniRoute sohbet modeli yok")
|
||||
kayitlar = {m["id"]: m for m in metadata if m.get("id")}
|
||||
ilanlar = {m["id"]: m for m in data if m.get("id")}
|
||||
nano = pencere.nano_yukle(home, ua, ag=ag) # ag=False: acilista yalniz onbellek
|
||||
ogr = pencere.ogrenilen(home)
|
||||
aracsiz = pencere.aracsizlar(home)
|
||||
for g in girdiler:
|
||||
bilgi = pencere.coz(g["id"], g["kind"], g.get("yol"), kayitlar.get(g["id"]), nano, ogr, aracsiz)
|
||||
bilgi = pencere.coz(g["id"], g["kind"], g.get("yol"), kayitlar.get(g["id"]), nano, ogr, aracsiz,
|
||||
ilan=ilanlar.get(g["id"]))
|
||||
g["pencere"], g["gorsel"], g["arac"], g["pencere_kaynak"] = (bilgi["pencere"], bilgi["gorsel"],
|
||||
bilgi["arac"], bilgi["kaynak"])
|
||||
g["claude"] = pencere.claude_var(g["kind"], g.get("yol"), kayitlar.get(g["id"]))
|
||||
|
||||
@@ -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 = "16"
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
try:
|
||||
with open(os.path.abspath(__file__), "rb") as _f:
|
||||
@@ -363,15 +363,40 @@ 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."""
|
||||
if isinstance(req.get("input"), list) and any(
|
||||
isinstance(item, dict) and item.get("type") == "agent_message" for item in req["input"]):
|
||||
import gecis_context
|
||||
req["input"] = gecis_context.portable_reasoning(req)["input"]
|
||||
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 +412,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 +573,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 +681,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 +708,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 +721,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
|
||||
@@ -980,13 +1035,13 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
if jev_denetim.muaf_mi(hedef):
|
||||
return self._stream(resp, resp.status, tag, rewriter, izle, basliksiz=True)
|
||||
if gecmedi:
|
||||
# Son katman ne dondurduyse o gider, hicbir sey eklenmez; dondurmediyse hicbir sey gitmez.
|
||||
if son_govde:
|
||||
w(son_govde)
|
||||
else:
|
||||
event = {"type": "response.failed", "response": {"status": "failed", "error": {
|
||||
"code": "jev_no_fallback", "message": "Jev denetiminden gecen cevap alinamadi; gecis kurallarini kontrol edin."}}}
|
||||
w(b"event: response.failed\ndata: " + json.dumps(event).encode() + b"\n\n")
|
||||
# Headers already declare SSE. A raw upstream JSON/HTML body
|
||||
# here leaves Codex waiting for a terminal event and retrying.
|
||||
event = {"type": "response.failed", "response": {
|
||||
"id": "resp_astrabridge_%d" % time.time_ns(),
|
||||
"object": "response", "status": "failed", "output": [], "error": {
|
||||
"code": "jev_no_fallback", "message": "Jev denetiminden gecen cevap alinamadi; gecis kurallarini kontrol edin."}}}
|
||||
w(b"event: response.failed\ndata: " + json.dumps(event).encode() + b"\n\n")
|
||||
else:
|
||||
for chunk in parcalar:
|
||||
if izle is not None:
|
||||
@@ -1623,11 +1678,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)
|
||||
@@ -1695,7 +1751,7 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
yanit = urllib.request.urlopen(urllib.request.Request(
|
||||
jev_rota["url"], data=json.dumps(katman_req, ensure_ascii=False).encode("utf-8"), headers=jev_hdrs, method="POST"),
|
||||
timeout=UPSTREAM_IDLE_TIMEOUT)
|
||||
return (yanit, SseRewriter(custom, hedef=ad, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren),
|
||||
return (yanit, SseRewriter(custom, hedef=ad, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren, tool_names=tool_names),
|
||||
f"[{n} {slug}=>{ad}]")
|
||||
# A 9Router combo (Fusion-*, no provider prefix) runs a whole panel upstream. Once it may
|
||||
# have started (timeout, 504, cut stream) resending it runs and bills it twice (28 Sep 2026);
|
||||
|
||||
@@ -221,7 +221,7 @@ def _yapraklar(dugum, derinlik=0):
|
||||
return cikti
|
||||
|
||||
|
||||
def coz(kimlik, tur, yol, kayit=None, nano=None, ogr=None, aracsiz=()):
|
||||
def coz(kimlik, tur, yol, kayit=None, nano=None, ogr=None, aracsiz=(), ilan=None):
|
||||
"""Bir katalog girdisinin ilan edilecek penceresi ve yetenekleri.
|
||||
kimlik: gecit kimligi; tur: "combo" | "model"; yol: gecidin aciklamasindaki "saglayici/model" yolu;
|
||||
kayit: /router/catalog girdisi (combo topolojisi icin). Donus: {"pencere","gorsel","arac","kaynak"}."""
|
||||
@@ -246,6 +246,13 @@ def coz(kimlik, tur, yol, kayit=None, nano=None, ogr=None, aracsiz=()):
|
||||
m = re.match(r"no-think-(?:9router-)?([a-z0-9]+)-", kimlik or "")
|
||||
yol = (m.group(1) + "/" + yol[len("no-think/"):]) if m else yol
|
||||
sonuc = yol_bilgisi(yol, nano) or dict(bilinmeyen)
|
||||
# Exact /models metadata is stronger evidence than name-based local
|
||||
# fallbacks. Do not infer a combo's capacity from its display name.
|
||||
ilan = ilan if isinstance(ilan, dict) else {}
|
||||
canli_sinirlar = [ilan[k] for k in ("context_length", "max_input_tokens")
|
||||
if type(ilan.get(k)) is int and TABAN <= ilan[k] <= 10_000_000]
|
||||
if canli_sinirlar:
|
||||
sonuc = {**sonuc, "pencere": min(canli_sinirlar), "kaynak": "saglayici-katalogu"}
|
||||
if kimlik in aracsiz:
|
||||
sonuc = {**sonuc, "arac": False}
|
||||
sinir = (ogr or {}).get(kimlik)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Regression fixture matching actual Codex child delivery, with no user data."""
|
||||
import copy
|
||||
import unittest
|
||||
|
||||
import gecis_context
|
||||
import kopru
|
||||
|
||||
|
||||
class AgentPayloadTests(unittest.TestCase):
|
||||
def request(self, text):
|
||||
return {"model": "ide/test", "input": [{
|
||||
"type": "agent_message", "author": "/root", "recipient": "/root/test",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "Message Type: NEW_TASK\nPayload:\n"},
|
||||
{"type": "encrypted_content", "encrypted_content": text},
|
||||
],
|
||||
}]}
|
||||
|
||||
def test_actual_peer_wrapper_survives_external_text_extraction(self):
|
||||
task = "Run exec_command: Write-Output (157 * 29); report the exit code."
|
||||
request = self.request(task)
|
||||
original = copy.deepcopy(request)
|
||||
kopru.flatten_request(request)
|
||||
# An OpenAI-compatible translator consumes input_text, not Codex's
|
||||
# encrypted_content part. Checking the JSON string alone misses loss.
|
||||
text = "\n".join(part["text"] for item in request["input"]
|
||||
for part in item["content"] if part["type"] == "input_text")
|
||||
self.assertIn(task, text)
|
||||
self.assertIn('/root/test', text)
|
||||
self.assertEqual(request["input"][0]["role"], "user")
|
||||
self.assertEqual(original["input"][0]["content"][1]["type"], "encrypted_content")
|
||||
|
||||
def test_peer_result_and_plain_parts_keep_order_without_source_mutation(self):
|
||||
request = self.request("Result: 4553; exit code: 0")
|
||||
original = copy.deepcopy(request)
|
||||
result = gecis_context.portable_reasoning(request)
|
||||
self.assertEqual(result["input"][0]["content"][-1]["text"], "Result: 4553; exit code: 0")
|
||||
self.assertEqual(request, original)
|
||||
|
||||
def test_opaque_peer_payload_fails_instead_of_sending_header_only(self):
|
||||
for opaque in ("gAAAA_native_ciphertext", "A" * 200, None):
|
||||
with self.subTest(opaque=opaque):
|
||||
with self.assertRaises(gecis_context.TransferError):
|
||||
gecis_context.portable_reasoning(self.request(opaque))
|
||||
|
||||
def test_reasoning_ciphertext_is_still_not_interpreted_as_a_peer_task(self):
|
||||
request = {"input": [{"type": "reasoning", "encrypted_content": "Run a command now"}]}
|
||||
self.assertEqual(gecis_context.portable_reasoning(request)["input"], [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -22,6 +22,26 @@ from test_model_switch import env
|
||||
|
||||
|
||||
class History(unittest.TestCase):
|
||||
def test_agent_task_survives_external_projection_and_recent_history(self):
|
||||
task = {"type": "agent_message", "id": "amsg_task", "author": "/root",
|
||||
"recipient": "/root/child", "content": [
|
||||
{"type": "input_text", "text": "Message Type: NEW_TASK\nRun CHILD_TOOL_410962"}]}
|
||||
req = {"input": [{"role": "user", "content": "Old setup"}, task]}
|
||||
before = copy.deepcopy(req)
|
||||
self.assertEqual(gc.normalize(req), before)
|
||||
projected = gc.portable_reasoning(req)
|
||||
message = projected["input"][-1]
|
||||
self.assertEqual(message["role"], "user")
|
||||
self.assertEqual(message["content"][1:], task["content"])
|
||||
self.assertIn('/root/child', message["content"][0]["text"])
|
||||
self.assertNotIn("id", message)
|
||||
self.assertEqual(gc.split_history(projected["input"], keep_users=1)[2], [message])
|
||||
self.assertEqual(req, before)
|
||||
self.assertEqual(gc.portable_reasoning(projected), projected)
|
||||
direct = copy.deepcopy(req)
|
||||
env["flatten_request"](direct)
|
||||
self.assertEqual(direct["input"][-1], message)
|
||||
|
||||
def test_native_function_results_are_not_foreign_or_renamed(self):
|
||||
req = {"input": [{"type": "function_call_output", "id": "fco_native",
|
||||
"call_id": "call_native", "output": "kept"}]}
|
||||
|
||||
@@ -335,7 +335,7 @@ class Akis(unittest.TestCase):
|
||||
self.assertNotIn(b"yapamam", self.h.wfile.getvalue())
|
||||
self.assertIn(b'response.failed', self.sent())
|
||||
|
||||
def test_the_last_layers_own_error_body_is_delivered_untouched(self):
|
||||
def test_last_layer_http_error_terminates_sse_without_leaking_raw_body(self):
|
||||
class Hata(Exception):
|
||||
def read(self):
|
||||
return b'{"error": {"message": "katman 4 hatasi"}}'
|
||||
@@ -344,7 +344,14 @@ class Akis(unittest.TestCase):
|
||||
def ac(ad):
|
||||
raise Hata("HTTP 400")
|
||||
self.h._denetimli_akis(Yanit(sse("Bunu yapamam.")), "[1]", None, None, "yap", ["Katman-4"], ac)
|
||||
self.assertEqual(self.sent(), b'{"error": {"message": "katman 4 hatasi"}}')
|
||||
raw = self.sent()
|
||||
self.assertTrue(raw.startswith(b"event: response.failed\ndata: "))
|
||||
event = json.loads(raw.split(b"data: ", 1)[1])
|
||||
self.assertEqual(event["response"]["status"], "failed")
|
||||
self.assertEqual(event["response"]["error"]["code"], "jev_no_fallback")
|
||||
self.assertEqual(event["response"]["output"], [])
|
||||
self.assertNotIn(b"katman 4 hatasi", raw)
|
||||
self.assertNotIn(b"Bunu yapamam", raw)
|
||||
|
||||
def test_unreachable_jev_keeps_the_answer(self):
|
||||
self.puanlar = [None]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import pencere
|
||||
|
||||
|
||||
def test_exact_combo_limits_override_unknown_and_local_defaults():
|
||||
row = {"context_length": 872000, "max_input_tokens": 872000, "max_output_tokens": 128000}
|
||||
result = pencere.coz("GPT-6-Astra", "combo", None, ilan=row)
|
||||
assert result["pencere"] == 872000
|
||||
assert result["kaynak"] == "saglayici-katalogu"
|
||||
result = pencere.coz("codex-gpt-6-astra", "model", "codex/gpt-6-astra", ilan=row)
|
||||
assert result["pencere"] == 872000
|
||||
|
||||
|
||||
def test_learned_actual_limit_still_wins_and_invalid_metadata_is_ignored():
|
||||
result = pencere.coz("combo", "combo", None, ilan={"context_length": 872000}, ogr={"combo": 500000})
|
||||
assert result["pencere"] == 500000 and result["kaynak"] == "ogrenilen"
|
||||
for value in (True, -1, "872000", 999999999):
|
||||
result = pencere.coz("combo", "combo", None, ilan={"context_length": value})
|
||||
assert result["pencere"] == pencere.VARSAYILAN
|
||||
@@ -43,7 +43,12 @@ class ModelSwitch(unittest.TestCase):
|
||||
{"type": "custom_tool_call_output", "id": "ctco_4827", "call_id": "call_4827", "output": "result-4827"},
|
||||
], "tools": [{"type": "namespace", "name": "functions", "tools": [{"type": "custom", "name": "probe"}]}]}
|
||||
req = copy.deepcopy(original)
|
||||
self.assertEqual(env["flatten_request"](req), {"probe"})
|
||||
names = {}
|
||||
custom = env["flatten_request"](req, names)
|
||||
alias = req["tools"][0]["name"]
|
||||
self.assertEqual(custom, {alias})
|
||||
self.assertEqual(names[alias], ("functions", "probe"))
|
||||
self.assertEqual(req["input"][0]["name"], alias)
|
||||
env["fix_tool_ids"](req)
|
||||
self.assertEqual(json.loads(req["input"][0]["arguments"]), {"input": "remember-4827"})
|
||||
self.assertEqual(req["input"][1]["output"], "result-4827")
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Namespace routing regressions: leaf-only calls cannot dispatch collaboration tools."""
|
||||
import copy
|
||||
import json
|
||||
import unittest
|
||||
from test_model_switch import env
|
||||
|
||||
|
||||
class ToolNamespaces(unittest.TestCase):
|
||||
def request(self):
|
||||
return {"tools": [
|
||||
{"type": "namespace", "name": "collaboration", "tools": [{"type": "function", "name": "spawn_agent", "parameters": {"type": "object", "properties": {}}}]},
|
||||
{"type": "namespace", "name": "other", "tools": [{"type": "function", "name": "spawn_agent"}]},
|
||||
{"type": "namespace", "name": "functions", "tools": [{"type": "custom", "name": "apply_patch"}]},
|
||||
{"type": "function", "name": "plain"}],
|
||||
"input": [{"type": "function_call", "namespace": "collaboration", "name": "spawn_agent", "id": "fc_a", "call_id": "call_a", "arguments": "{}"},
|
||||
{"type": "function_call_output", "call_id": "call_a", "output": "child_123"}]}
|
||||
|
||||
def test_namespace_calls_round_trip_at_all_stream_stages(self):
|
||||
req = self.request(); original = copy.deepcopy(req); names = {}
|
||||
custom = env["flatten_request"](req, names)
|
||||
alias = req["tools"][0]["name"]
|
||||
self.assertNotEqual(alias, req["tools"][1]["name"])
|
||||
self.assertLessEqual(len(alias), 64)
|
||||
self.assertEqual(req["input"][0]["name"], alias)
|
||||
self.assertNotIn("namespace", req["input"][0])
|
||||
self.assertEqual(req["input"][1], original["input"][1])
|
||||
adapter = env["SseRewriter"](custom, tool_names=names)
|
||||
item = {"type": "function_call", "name": alias, "id": "fc_test", "call_id": "call_test", "arguments": "{}"}
|
||||
for stage in ["response.output_item.added", "response.output_item.done"]:
|
||||
restored = adapter._fix({"type": stage, "item": dict(item)})["item"]
|
||||
self.assertEqual((restored["namespace"], restored["name"]), ("collaboration", "spawn_agent"))
|
||||
self.assertEqual(restored["call_id"], "call_test")
|
||||
restored = adapter._fix({"type": "response.completed", "response": {"output": [dict(item)]}})["response"]["output"][0]
|
||||
self.assertEqual((restored["namespace"], restored["name"]), ("collaboration", "spawn_agent"))
|
||||
|
||||
def test_custom_tool_namespace_and_ids_survive_chunked_sse(self):
|
||||
req = self.request(); names = {}; custom = env["flatten_request"](req, names)
|
||||
alias = req["tools"][2]["name"]
|
||||
item = {"type": "function_call", "name": alias, "id": "fc_patch", "call_id": "call_patch", "arguments": json.dumps({"input": "patch text"})}
|
||||
raw = ("data: " + json.dumps({"type": "response.output_item.done", "item": item}) + "\n\n").encode()
|
||||
a = env["SseRewriter"](custom, tool_names=names)
|
||||
output = a.feed(raw[:31]) + a.feed(raw[31:]) + a.flush()
|
||||
restored = json.loads(next(l[6:] for l in output.decode().splitlines() if l.startswith("data: ")))["item"]
|
||||
self.assertEqual((restored["namespace"], restored["name"], restored["type"]), ("functions", "apply_patch", "custom_tool_call"))
|
||||
self.assertEqual(restored["id"], "ctc_patch")
|
||||
self.assertEqual(restored["input"], "patch text")
|
||||
|
||||
def test_plain_tools_stay_plain_and_history_not_mutated(self):
|
||||
req = self.request(); original = copy.deepcopy(req); old_item = req["input"][0]
|
||||
env["flatten_request"](req, {})
|
||||
self.assertEqual(req["tools"][-1]["name"], "plain")
|
||||
self.assertEqual(old_item, original["input"][0])
|
||||
plain = {"type": "function_call", "name": "plain", "arguments": "{}"}
|
||||
self.assertEqual(env["SseRewriter"](set())._fix({"item": plain})["item"], plain)
|
||||
|
||||
def test_additional_tools_long_names_and_tool_choice(self):
|
||||
namespace = "provider_" * 20
|
||||
req = {"tools": [], "input": [{"type": "additional_tools", "tools": [{"type": "namespace", "name": namespace, "tools": [{"type": "function", "name": "read"}]}]}], "tool_choice": {"type": "function", "namespace": namespace, "name": "read"}}
|
||||
names = {}; env["flatten_request"](req, names)
|
||||
alias = req["tools"][0]["name"]
|
||||
self.assertLessEqual(len(alias), 64)
|
||||
self.assertEqual(req["tool_choice"], {"type": "function", "name": alias})
|
||||
self.assertEqual(req["input"], [])
|
||||
self.assertEqual(names[alias], (namespace, "read"))
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -27,7 +27,7 @@ export const CODEX_LINK_REFRESH_URL = `${CODEX_LINK_ORIGIN}/aiturk/codex-baglama
|
||||
export const CODEX_LINK_MARKER = '# AITURK IDE Codex Baglama'
|
||||
// Must equal SURUM in assets/codex-bridge/bridge.py; a running bridge that reports
|
||||
// another version is restarted so an updated IDE never keeps an old bridge alive.
|
||||
export const CODEX_BRIDGE_VERSION = '13'
|
||||
export const CODEX_BRIDGE_VERSION = '16'
|
||||
export const CODEX_LINK_LINE = `openai_base_url = "${CODEX_LINK_ORIGIN}/backend-api/codex" ${CODEX_LINK_MARKER}`
|
||||
|
||||
export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "aiturk-ide",
|
||||
"productName": "AITURK IDE",
|
||||
"private": true,
|
||||
"version": "1.0.0-beta.32",
|
||||
"version": "1.0.0-beta.35",
|
||||
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
|
||||
"author": "AITURK / TurkServis",
|
||||
"repository": {
|
||||
|
||||
Generated
+1
-1
@@ -65,7 +65,7 @@
|
||||
},
|
||||
"apps/desktop": {
|
||||
"name": "aiturk-ide",
|
||||
"version": "1.0.0-beta.32",
|
||||
"version": "1.0.0-beta.35",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@assistant-ui/core": "0.2.23",
|
||||
|
||||
Reference in new issue
Block a user