Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f13b86e5f | ||
|
|
7a479e46be | ||
|
|
db46a470ea | ||
|
|
21a8f3b8e9 | ||
|
|
fb62f31be3 | ||
|
|
1bf80e06a3 | ||
|
|
1aa033a751 | ||
|
|
212015e7c9 | ||
|
|
65ce4be58b | ||
|
|
69e2f98e18 | ||
|
|
0d8c521a3c |
No files matched your search
@@ -5,6 +5,10 @@ on:
|
||||
branches: [aiturk/main, fix/ide-windows-vcruntime]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -19,6 +23,16 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
github-server-url: https://gitea.turkservis.online
|
||||
- name: Require main and Windows release to share the exact source
|
||||
run: |
|
||||
set -eu
|
||||
main_sha="$(git ls-remote origin refs/heads/aiturk/main | cut -f1)"
|
||||
release_sha="$(git ls-remote origin refs/heads/fix/ide-windows-vcruntime | cut -f1)"
|
||||
if [ "$main_sha" != "$GITHUB_SHA" ] || [ "$release_sha" != "$GITHUB_SHA" ]; then
|
||||
echo "Both IDE branches must be updated together to the same merged commit."
|
||||
echo "Use one atomic push; refusing to publish a divergent branch."
|
||||
exit 1
|
||||
fi
|
||||
- name: Exact source and Linux test environment
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
@@ -27,6 +27,33 @@ Run: `python bridge.py --home <data dir> --hermes-home <agent home>`. The IDE wr
|
||||
|
||||
Tests: `python -m unittest discover` in this directory.
|
||||
|
||||
## Version 24: portable model switches and summary coverage (IDE beta.45)
|
||||
|
||||
Cross-provider requests replace encrypted reasoning and every typed encrypted
|
||||
peer-message part with a short visible omission note. Ciphertext is never
|
||||
interpreted, guessed from its alphabet, or forwarded to another provider. Visible
|
||||
messages, peer text, tool calls/results and their order remain intact. Projection
|
||||
is idempotent and changes only the outgoing copy. Returning to a subscription
|
||||
model retains its original encrypted execution state, including large histories.
|
||||
Actual conversation compaction is still rendered through its issuing endpoint;
|
||||
unreadable conversation summaries retain the existing explicit error.
|
||||
|
||||
Other external-provider switches reuse the same transcript, with destination
|
||||
context preparation only when necessary. This follows the Hermes runtime model
|
||||
switch contract: `agent/agent_runtime_helpers.py::switch_model` updates the client,
|
||||
provider, cache policy and compressor without resetting conversation history;
|
||||
`website/docs/user-guide/configuring-models.md` documents in-session `/model`.
|
||||
Native voice routing is unchanged.
|
||||
|
||||
The compression target is a maximum, not a minimum length. All of the available
|
||||
target budget is offered to the summarizer; compression level controls prose
|
||||
density and no longer multiplies that budget down a second time. Numbered
|
||||
decision identifiers, file references and numeric settings must survive the
|
||||
summary before it is accepted or cached. An incomplete provider summary tries
|
||||
the configured fallback; a deficient old checkpoint is ignored and rebuilt.
|
||||
These literal checks detect concrete omissions; they do not prove semantic
|
||||
equivalence of every sentence.
|
||||
|
||||
## Version 19: decision stages (14 October 2026)
|
||||
|
||||
The decision model (Jev or another System One compatible service such as Celeris) can now be asked at
|
||||
|
||||
@@ -35,7 +35,7 @@ import gecis_context
|
||||
import terminal_stream
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SURUM = "21" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
|
||||
SURUM = "24" # 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]
|
||||
@@ -537,6 +537,8 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
if external or mixed or gecis_context.foreign(req):
|
||||
try:
|
||||
normalized = gecis_context.normalize(req)
|
||||
if external:
|
||||
normalized = gecis_context.portable_reasoning(normalized)
|
||||
except gecis_context.TransferError as e:
|
||||
self._transfer_begin()
|
||||
return self._transfer_fail(str(e))
|
||||
|
||||
@@ -142,6 +142,7 @@ def call(handler, module, slug, source, budget, settings, deadline, depth=0):
|
||||
text = ozet_kopru.sse_metni(bytes(raw))
|
||||
if not text or len(text.strip()) < 80:
|
||||
raise gc.TransferError('Sıkıştırıcı tamamlanmış bir özet döndürmedi')
|
||||
gc.require_summary_coverage(chunk, text)
|
||||
results.append(text)
|
||||
combined = '\n\n'.join(results)
|
||||
if not combined:
|
||||
@@ -211,6 +212,8 @@ def prepare(handler, module, payload, checkpoints):
|
||||
protected, old, recent = gc.split_history(req.get('input') or [], settings['keepTurns'])
|
||||
checkpoints.begin(scope, old, settings)
|
||||
saved = checkpoints.get(scope, old, settings) if old else None
|
||||
if saved and gc.summary_missing(old[:saved[0]], saved[1]):
|
||||
saved = None
|
||||
effective = dict(req, input=protected + [ozet_kopru._ozet_mesaji(saved[1])] + old[saved[0]:] + recent) if saved else req
|
||||
if not compact and gc.tokens(effective) <= trigger:
|
||||
return {'request': effective, 'window': window}
|
||||
|
||||
@@ -12,11 +12,52 @@ import threading
|
||||
from collections import OrderedDict
|
||||
|
||||
import ozet_kopru
|
||||
import compression_settings
|
||||
|
||||
PREFIXES = {"custom_tool_call": "ctc", "custom_tool_call_output": "ctco",
|
||||
"function_call": "fc", "function_call_output": "fco"}
|
||||
RATIO = 0.70
|
||||
PEER_OMITTED = "[Aktarım notu: Burada okunamayan şifreli bir alt ajan iletisi vardı; aktarılmadı.]"
|
||||
REASONING_OMITTED = "[Aktarım notu: Şifreli model düşünme kaydı aktarılmadı.]"
|
||||
|
||||
|
||||
def summary_missing(source, summary):
|
||||
"""Check literal decisions, file paths and numeric settings, never secrets.
|
||||
|
||||
This is a loss detector for concrete references, not a semantic proof of
|
||||
every sentence. Missing references must not enter a durable checkpoint.
|
||||
"""
|
||||
if isinstance(source, str):
|
||||
try:
|
||||
source = json.loads(source)
|
||||
except ValueError:
|
||||
pass
|
||||
def strings(value):
|
||||
if isinstance(value, str):
|
||||
yield value
|
||||
elif isinstance(value, dict):
|
||||
for child in value.values():
|
||||
yield from strings(child)
|
||||
elif isinstance(value, list):
|
||||
for child in value:
|
||||
yield from strings(child)
|
||||
text = '\n'.join(strings(source)).replace('\\', '/')
|
||||
target = summary.replace('\\', '/').replace('`', '')
|
||||
identifiers = set(re.findall(r'\b[A-Z][A-Z0-9]{0,23}(?:-[A-Z][A-Z0-9]{0,23}){0,3}-\d[A-Za-z0-9_-]{0,63}\b', text))
|
||||
identifiers = {x for x in identifiers if not re.match(r'(?:SECRET|TOKEN|APIKEY|PASSWORD|OTP)-', x)}
|
||||
paths = set(re.findall(r'(?<![A-Za-z0-9_.-])(?:[A-Za-z]:/(?:[A-Za-z0-9_.-]{1,160}/){0,24}|(?:[A-Za-z0-9_.-]{1,160}/){1,24})[A-Za-z0-9_.-]{1,160}\.(?:json|py|tsx?|jsx?|md|txt|ya?ml|toml|ini|sql|csv|log)\b', text)) if '/' in text else set()
|
||||
assignments = set(re.findall(r'\b([A-Za-z_][A-Za-z0-9_]{0,63})\s*=\s*(\d{1,16}|true|false)\b', text))
|
||||
assignments = {(key, value) for key, value in assignments
|
||||
if not re.search(r'key|token|secret|password|sifre|anahtar|otp', key, re.I)}
|
||||
missing = sum(value not in target for value in identifiers | paths)
|
||||
missing += sum(not re.search(re.escape(key) + r'\s*[=:]\s*' + re.escape(value) + r'\b', target)
|
||||
for key, value in assignments)
|
||||
return missing
|
||||
|
||||
|
||||
def require_summary_coverage(source, summary):
|
||||
missing = summary_missing(source, summary)
|
||||
if missing:
|
||||
raise TransferError(f'Özet {missing} karar, dosya yolu veya sayısal ayarı korumadı; eksik özet kabul edilmedi.')
|
||||
|
||||
|
||||
class TransferError(ValueError):
|
||||
@@ -56,11 +97,16 @@ def foreign(req):
|
||||
|
||||
def native_opaque(req):
|
||||
"""Native encrypted state belongs to the subscription, even in mixed history."""
|
||||
return any(isinstance(item, dict) and item.get("type") == "compaction" and item.get("encrypted_content")
|
||||
and not str(item["encrypted_content"]).startswith(ozet_kopru.ISARET)
|
||||
return any(isinstance(item, dict) and (
|
||||
(item.get("type") in ("compaction", "reasoning") and item.get("encrypted_content")
|
||||
and not str(item["encrypted_content"]).startswith(ozet_kopru.ISARET))
|
||||
or (item.get("type") == "agent_message" and any(
|
||||
isinstance(part, dict) and part.get("type") == "encrypted_content"
|
||||
for part in item.get("content") or [])))
|
||||
for item in req.get("input") or [])
|
||||
|
||||
|
||||
|
||||
def foreign_history(req):
|
||||
"""Semantic migration evidence, excluding tool ID format mismatches."""
|
||||
return any(isinstance(item, dict) and (
|
||||
@@ -135,14 +181,10 @@ def portable_reasoning(req):
|
||||
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})
|
||||
# Treat the typed wrapper as opaque regardless of whether
|
||||
# its bytes happen to look like plaintext. Never decrypt,
|
||||
# guess, or forward it to a different provider.
|
||||
portable.append({"type": "input_text", "text": PEER_OMITTED})
|
||||
else:
|
||||
portable.append(part)
|
||||
result.append({"type": "message", "role": "user", "content": [
|
||||
@@ -158,6 +200,8 @@ def portable_reasoning(req):
|
||||
for part in item.get(field) or []:
|
||||
if isinstance(part, dict) and isinstance(part.get("text"), str) and part["text"]:
|
||||
visible.append(part["text"])
|
||||
if item.get("encrypted_content"):
|
||||
visible.append(REASONING_OMITTED)
|
||||
if visible:
|
||||
# Keep this on the assistant side: a synthetic user message would
|
||||
# change split_history's count of protected user turns.
|
||||
@@ -214,6 +258,8 @@ class Preparer:
|
||||
protected, old, recent = split_history(req.get("input") or [], keep)
|
||||
original_old = old
|
||||
saved = checkpoints.get(scope, old, settings) if checkpoints and old else None
|
||||
if saved and summary_missing(old[:saved[0]], saved[1]):
|
||||
saved = None
|
||||
if saved:
|
||||
old = [ozet_kopru._ozet_mesaji(saved[1])] + old[saved[0]:]
|
||||
req = dict(req, input=protected + old + recent)
|
||||
@@ -233,8 +279,10 @@ class Preparer:
|
||||
if any(isinstance(x, dict) and x.get("encrypted_content") for x in old):
|
||||
raise TransferError("Şifreli geçmiş önce sahibi tarafından okunabilir özete çevrilmeli.")
|
||||
source = encoded(old)
|
||||
factor = compression_settings.LEVELS[settings['level']] if settings else .5
|
||||
budget = max(512, int(available * factor))
|
||||
# The configured target already bounds the resulting context. Applying
|
||||
# the level's ratio again denied the summarizer most of that budget.
|
||||
# Level controls writing style; the whole available budget stays usable.
|
||||
budget = max(512, available)
|
||||
key = hashlib.sha256((scope + str(budget) + encoded(settings) + source).encode()).hexdigest()
|
||||
with self.lock:
|
||||
summary = self.cache.get(key)
|
||||
@@ -242,6 +290,7 @@ class Preparer:
|
||||
summary = summarize(source, budget)
|
||||
if not isinstance(summary, str) or len(summary.strip()) < 80:
|
||||
raise TransferError("AITURK özeti tamamlanamadı; eski geçmiş korundu.")
|
||||
require_summary_coverage(old, summary)
|
||||
candidate = dict(req, input=protected + [ozet_kopru._ozet_mesaji(summary)] + recent)
|
||||
if tokens(candidate) > limit:
|
||||
raise TransferError("Hazırlanan özet hedef bütçesine sığmadı; eski geçmiş korundu.")
|
||||
|
||||
@@ -59,6 +59,42 @@ def _https(value):
|
||||
return url.scheme == 'https' and bool(url.netloc) and not url.username and not url.password
|
||||
|
||||
|
||||
IDE_PREFIX = 'ide/'
|
||||
|
||||
|
||||
def _strip_ide(value):
|
||||
"""A stored model name must never carry the gateway's own 'ide/' prefix; the bridge adds it when
|
||||
it routes. A doubled 'ide/ide/...' makes the bridge treat the name as an unknown model, read its
|
||||
context window as the 32768 default and silently drop the request. No real provider/model id
|
||||
begins with 'ide/', so stripping a leading 'ide/' here can only undo an accidental prefix."""
|
||||
if isinstance(value, str):
|
||||
while value.startswith(IDE_PREFIX):
|
||||
value = value[len(IDE_PREFIX):]
|
||||
return value
|
||||
|
||||
|
||||
def _normalize_models(settings):
|
||||
"""Strip a stray 'ide/' prefix from every stored model name so routing can never double it."""
|
||||
if 'fallbackModel' in settings:
|
||||
settings['fallbackModel'] = _strip_ide(settings['fallbackModel'])
|
||||
for key in ('watchModels', 'exemptModels'):
|
||||
values = settings.get(key)
|
||||
if isinstance(values, list):
|
||||
settings[key] = [_strip_ide(x) for x in values]
|
||||
prefilter = settings.get('prefilter')
|
||||
if isinstance(prefilter, dict) and 'target' in prefilter:
|
||||
prefilter['target'] = _strip_ide(prefilter['target'])
|
||||
auto = settings.get('autoRoute')
|
||||
if isinstance(auto, dict):
|
||||
if 'model' in auto:
|
||||
auto['model'] = _strip_ide(auto['model'])
|
||||
targets = auto.get('targets')
|
||||
if isinstance(targets, dict):
|
||||
for key in list(targets):
|
||||
targets[key] = _strip_ide(targets[key])
|
||||
return settings
|
||||
|
||||
|
||||
def validate(settings):
|
||||
"""Raise ValueError when a value could route to the wrong place or wait without bound."""
|
||||
# A corrupted file must not silently change routing or contact another host.
|
||||
@@ -115,6 +151,7 @@ def _policy(settings):
|
||||
for key in POLICY_KEYS:
|
||||
if isinstance(overrides.get(key), dict):
|
||||
merged[key] = _fill(overrides[key], settings[key])
|
||||
_normalize_models(merged)
|
||||
return validate(merged)
|
||||
except (OSError, ValueError, KeyError, TypeError, AttributeError):
|
||||
return settings
|
||||
@@ -129,6 +166,7 @@ def read(overlay=True):
|
||||
settings = data['settings']
|
||||
for section in SECTIONS:
|
||||
settings[section] = _fill(settings[section], DEFAULTS[section]) if section in settings else copy.deepcopy(DEFAULTS[section])
|
||||
_normalize_models(settings)
|
||||
validate(settings)
|
||||
return (_policy(settings) if overlay else settings), data.get('protectedKey')
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ from bekci_watch import MetadataFilter
|
||||
# Installed only by owned native launcher, never an HTTP control endpoint.
|
||||
WATCH_OWNER = None
|
||||
|
||||
SURUM = "20"
|
||||
SURUM = "21"
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
try:
|
||||
with open(os.path.abspath(__file__), "rb") as _f:
|
||||
@@ -883,11 +883,27 @@ class SseRewriter:
|
||||
self.bekleyen = d
|
||||
return once
|
||||
return once + [d]
|
||||
def _restore_tool_name(self, item):
|
||||
def _restore_tool_name(self, item, plaintext=False):
|
||||
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
|
||||
if (plaintext and item.get("type") == "function_call"
|
||||
and item.get("namespace") == "collaboration"
|
||||
and item.get("name") in ("spawn_agent", "send_message", "followup_task")
|
||||
and "encrypted_function_args" not in item):
|
||||
# Codex distinguishes missing encryption metadata (opaque) from
|
||||
# an explicit empty list (plaintext). External providers return
|
||||
# ordinary JSON strings; stamp only a complete readable message,
|
||||
# after its final arguments arrive. Never override owner state.
|
||||
try:
|
||||
message = json.loads(item.get("arguments") or "{}").get("message")
|
||||
except (ValueError, AttributeError, TypeError):
|
||||
message = None
|
||||
if (isinstance(message, str) and message.strip()
|
||||
and not message.startswith("gAAAA")
|
||||
and not re.fullmatch(r"[A-Za-z0-9_+/=-]{128,}", message)):
|
||||
item["encrypted_function_args"] = []
|
||||
return item
|
||||
def _fix(self, d):
|
||||
t = d.get("type", "")
|
||||
@@ -923,10 +939,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(self._restore_tool_name(item))
|
||||
out.append(self._restore_tool_name(item, plaintext=True))
|
||||
d["response"]["output"] = out
|
||||
fix_tool_ids({"input": out})
|
||||
self._restore_tool_name(d.get("item"))
|
||||
self._restore_tool_name(d.get("item"), plaintext=t == "response.output_item.done")
|
||||
return d
|
||||
def feed(self, chunk):
|
||||
self.buf += chunk
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Regression fixture matching actual Codex child delivery, with no user data."""
|
||||
import copy
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import gecis_context
|
||||
import kopru
|
||||
from test_model_switch import env as wire_bridge
|
||||
|
||||
|
||||
class AgentPayloadTests(unittest.TestCase):
|
||||
@@ -16,16 +17,17 @@ class AgentPayloadTests(unittest.TestCase):
|
||||
],
|
||||
}]}
|
||||
|
||||
def test_actual_peer_wrapper_survives_external_text_extraction(self):
|
||||
def test_typed_encrypted_wrapper_is_omitted_even_if_bytes_look_like_text(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)
|
||||
wire_bridge["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.assertNotIn(task, text)
|
||||
self.assertIn(gecis_context.PEER_OMITTED, text)
|
||||
self.assertIn('/root/test', text)
|
||||
self.assertEqual(request["input"][0]["role"], "user")
|
||||
self.assertEqual(original["input"][0]["content"][1]["type"], "encrypted_content")
|
||||
@@ -34,18 +36,69 @@ class AgentPayloadTests(unittest.TestCase):
|
||||
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(result["input"][0]["content"][-1]["text"], gecis_context.PEER_OMITTED)
|
||||
self.assertEqual(result["input"][0]["content"][1], original["input"][0]["content"][0])
|
||||
self.assertEqual(request, original)
|
||||
|
||||
def test_opaque_peer_payload_fails_instead_of_sending_header_only(self):
|
||||
def test_opaque_peer_payload_is_replaced_by_a_visible_note(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))
|
||||
result = gecis_context.portable_reasoning(self.request(opaque))
|
||||
self.assertEqual(result["input"][0]["content"][-1]["text"], gecis_context.PEER_OMITTED)
|
||||
self.assertNotIn("encrypted_content", json.dumps(result))
|
||||
|
||||
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"], [])
|
||||
result = gecis_context.portable_reasoning(request)
|
||||
self.assertNotIn("Run a command now", json.dumps(result))
|
||||
self.assertEqual(result["input"][0]["content"][0]["text"], gecis_context.REASONING_OMITTED)
|
||||
|
||||
|
||||
class PlaintextAgentWireTests(unittest.TestCase):
|
||||
def item(self, message="Read the complete task and report its result.", **extra):
|
||||
return dict(type="function_call", namespace="collaboration", name="spawn_agent",
|
||||
id="fc_peer", call_id="call_peer", arguments=json.dumps({
|
||||
"task_name": "peer", "message": message}), **extra)
|
||||
|
||||
def test_final_plaintext_peer_calls_are_explicit_without_argument_changes(self):
|
||||
for name in ("spawn_agent", "send_message", "followup_task"):
|
||||
original = self.item(); original["name"] = name
|
||||
for stage in ("response.output_item.done", "response.completed"):
|
||||
with self.subTest(name=name, stage=stage):
|
||||
adapter = wire_bridge["SseRewriter"](set())
|
||||
item = copy.deepcopy(original)
|
||||
event = ({"type": stage, "item": item} if stage.endswith(".done") else
|
||||
{"type": stage, "response": {"output": [item]}})
|
||||
adapter._fix(event)
|
||||
self.assertEqual(item.pop("encrypted_function_args"), [])
|
||||
self.assertEqual(item, original)
|
||||
|
||||
def test_partial_empty_or_opaque_messages_never_gain_plaintext_metadata(self):
|
||||
for arguments in ('', '{"message":', '{}', '[]', 'null',
|
||||
json.dumps({"message": " "}), json.dumps({"message": None}),
|
||||
json.dumps({"message": "gAAAA_ciphertext"}),
|
||||
json.dumps({"message": "A" * 200})):
|
||||
for stage in ("response.output_item.added", "response.output_item.done", "response.completed"):
|
||||
with self.subTest(arguments=arguments, stage=stage):
|
||||
item = self.item(); item["arguments"] = arguments
|
||||
wire_bridge["SseRewriter"](set())._restore_tool_name(item, plaintext=stage != "response.output_item.added")
|
||||
self.assertNotIn("encrypted_function_args", item)
|
||||
item = self.item()
|
||||
wire_bridge["SseRewriter"](set())._fix({"type": "response.output_item.added", "item": item})
|
||||
self.assertNotIn("encrypted_function_args", item)
|
||||
|
||||
def test_existing_encryption_metadata_and_other_tools_are_untouched(self):
|
||||
for marker in (None, [], ["message"]):
|
||||
item = self.item(encrypted_function_args=marker)
|
||||
original = copy.deepcopy(item)
|
||||
wire_bridge["SseRewriter"](set())._fix({"type": "response.output_item.done", "item": item})
|
||||
self.assertEqual(item, original)
|
||||
for namespace, name in (("other", "spawn_agent"), ("collaboration", "wait_agent"),
|
||||
(None, "spawn_agent")):
|
||||
item = self.item(); item.update(namespace=namespace, name=name)
|
||||
original = copy.deepcopy(item)
|
||||
wire_bridge["SseRewriter"](set())._fix({"type": "response.output_item.done", "item": item})
|
||||
self.assertEqual(item, original)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -68,7 +68,7 @@ class History(unittest.TestCase):
|
||||
self.assertNotIn("OPAQUE_", json.dumps(result))
|
||||
self.assertEqual(req, before)
|
||||
self.assertLessEqual(gc.tokens(result), gc.input_budget(req, 32768))
|
||||
self.assertEqual(result["input"][-1], {"role": "user", "content": "Latest"})
|
||||
self.assertIn({"role": "user", "content": "Latest"}, result["input"])
|
||||
|
||||
def test_legacy_summary_and_tool_ids_repaired_without_mutating_source(self):
|
||||
req = {"input": [ozet_kopru.ozet_ogesi("KARAR-4827 korunacak"),
|
||||
@@ -259,19 +259,18 @@ class PreparationService(unittest.TestCase):
|
||||
self.assertNotIn("OPAQUE_REASONING", json.dumps(result))
|
||||
self.assertEqual(req, before)
|
||||
|
||||
def test_large_native_return_summarizes_visible_history_without_ciphertext(self):
|
||||
def test_large_native_return_keeps_original_encrypted_execution_state(self):
|
||||
for opaque in ({"type": "reasoning", "encrypted_content": "OPAQUE_NATIVE"},
|
||||
{"type": "agent_message", "content": [
|
||||
{"type": "encrypted_content", "encrypted_content": "OPAQUE_PEER"}]}):
|
||||
req = {"model": "gpt-6-astra", "input": [
|
||||
{"role": "user", "content": "KARAR-4827 " + "x" * 190000},
|
||||
{"type": "reasoning", "encrypted_content": "OPAQUE_NATIVE"},
|
||||
{"role": "user", "content": "Latest"}]}
|
||||
opaque, {"role": "user", "content": "Latest"}]}
|
||||
before = copy.deepcopy(req)
|
||||
result = self.service._prepare_transfer({"request": req, "source_model": "ide/test"})["request"]
|
||||
self.assertEqual(result["model"], "gpt-6-astra")
|
||||
self.assertIn("KARAR-4827", json.dumps(result))
|
||||
self.assertLessEqual(gc.tokens(result), gc.input_budget(req, 128000))
|
||||
self.assertEqual(result, before)
|
||||
self.assertEqual(req, before)
|
||||
self.assertTrue(self.server.gelen)
|
||||
self.assertTrue(all(b"OPAQUE_NATIVE" not in call["govde"] for call in self.server.gelen))
|
||||
self.assertEqual(self.server.gelen, [])
|
||||
|
||||
def test_external_compaction_uses_external_summary_not_subscription(self):
|
||||
req = {"model": "ide/test", "input": [{"role": "assistant", "content": "KARAR-4827"}, {"role": "user", "content": "KARAR-4827"}, {"type": "compaction_trigger"}]}
|
||||
@@ -281,6 +280,50 @@ class PreparationService(unittest.TestCase):
|
||||
|
||||
|
||||
class SocketMigration(fixtures.Ayirici):
|
||||
def test_opaque_peer_transfer_succeeds_without_ciphertext_or_transfer_error(self):
|
||||
req = {"model": "ide/agy-gemini-3.8-flash-high", "prompt_cache_key": "opaque-peer",
|
||||
"input": [{"role": "user", "content": "USER_DECISION"},
|
||||
{"type": "agent_message", "author": "/root", "recipient": "/root/peer",
|
||||
"content": [{"type": "input_text", "text": "VISIBLE_PEER_HEADER"},
|
||||
{"type": "encrypted_content", "encrypted_content": "gAAAA_synthetic_opaque"}]},
|
||||
{"role": "assistant", "content": "VISIBLE_ANSWER"},
|
||||
{"type": "function_call", "id": "fc_1", "call_id": "c1", "name": "read", "arguments": "{}"},
|
||||
{"type": "function_call_output", "id": "fco_1", "call_id": "c1", "output": "VISIBLE_RESULT"}]}
|
||||
original = copy.deepcopy(req)
|
||||
for model in (req["model"], "ide/another-provider"):
|
||||
req["model"] = model
|
||||
status, body, _ = self.iste("POST", "/backend-api/codex/responses", json.dumps(req).encode())
|
||||
self.assertEqual(status, 200)
|
||||
self.assertNotIn(b"aiturk_transfer_failed", body)
|
||||
sent = json.loads(self.kopru.gelen[-1]["govde"])
|
||||
wire = json.dumps(sent, ensure_ascii=False)
|
||||
self.assertNotIn("gAAAA_synthetic_opaque", wire)
|
||||
self.assertNotIn("encrypted_content", wire)
|
||||
self.assertIn(gc.PEER_OMITTED, wire)
|
||||
self.assertIn("VISIBLE_PEER_HEADER", wire)
|
||||
for item in original["input"][:1] + original["input"][2:]:
|
||||
self.assertIn(item, sent["input"])
|
||||
self.assertEqual(req["input"], original["input"])
|
||||
self.assertEqual(self.ozgun.gelen, [])
|
||||
# Returning uses the client's original transcript, including opaque peer
|
||||
# state. Projection never rewrites the persisted source or a native turn.
|
||||
req["model"] = "gpt-6-astra"
|
||||
raw = zstd(json.dumps(req).encode())
|
||||
status, _, _ = self.iste("POST", "/backend-api/codex/responses", raw, {"Content-Encoding": "zstd"})
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(self.ozgun.gelen[-1]["govde"], raw)
|
||||
|
||||
def test_native_opaque_peer_history_remains_byte_identical_on_owner_route(self):
|
||||
req = {"model": "gpt-6-astra", "input": [{"type": "agent_message", "author": "/root",
|
||||
"recipient": "/root/peer", "content": [{"type": "encrypted_content",
|
||||
"encrypted_content": "gAAAA_synthetic_native_owner"}]}]}
|
||||
raw = zstd(json.dumps(req).encode())
|
||||
status, _, _ = self.iste("POST", "/backend-api/codex/responses", raw,
|
||||
{"Content-Encoding": "zstd"})
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(self.ozgun.gelen[-1]["govde"], raw)
|
||||
self.assertEqual(self.kopru.gelen, [])
|
||||
|
||||
def test_completed_return_ends_preparation_for_following_native_turns(self):
|
||||
self.ozgun.cevap = duz(200, b'data: {"type":"response.completed"}\n\n', 'text/event-stream')
|
||||
req = {"model": "ide/test", "prompt_cache_key": "return-once", "input": []}
|
||||
|
||||
@@ -42,6 +42,28 @@ class Settings(unittest.TestCase):
|
||||
self.save(rejectionRules=[{'from': 'A', 'to': 'B'}, {'from': 'B', 'to': 'A'}])
|
||||
self.assertEqual(jev_denetim.sonrakiler('A'), ['B'])
|
||||
|
||||
def test_stored_ide_prefix_on_model_names_is_stripped(self):
|
||||
# A hand- or agent-edited settings file may store a model name carrying the gateway's own
|
||||
# 'ide/' prefix. The bridge adds 'ide/' again when it routes, so a stored prefix would make
|
||||
# 'ide/ide/...', an unknown model whose context window the bridge mis-reads before silently
|
||||
# dropping the request. read() must strip the stray prefix so routing stays correct.
|
||||
self.save(
|
||||
fallbackModel='ide/abliteration-abliterated-model-large-v2',
|
||||
exemptModels=['ide/claude-claude-opus-5-5', 'agnes-agnes-1.5-flash'],
|
||||
prefilter={**self.settings['prefilter'], 'target': 'ide/ide/abliteration-abliterated-model-large-v2'},
|
||||
)
|
||||
settings, _ = jev_settings.read()
|
||||
self.assertEqual(settings['fallbackModel'], 'abliteration-abliterated-model-large-v2')
|
||||
self.assertEqual(settings['prefilter']['target'], 'abliteration-abliterated-model-large-v2')
|
||||
self.assertEqual(settings['exemptModels'], ['claude-claude-opus-5-5', 'agnes-agnes-1.5-flash'])
|
||||
self.assertTrue(jev_denetim.muaf_mi('claude-claude-opus-5-5'))
|
||||
|
||||
def test_strip_ide_leaves_normal_names_and_removes_only_prefix(self):
|
||||
self.assertEqual(jev_settings._strip_ide('claude-claude-opus-5-5'), 'claude-claude-opus-5-5')
|
||||
self.assertEqual(jev_settings._strip_ide('ide/x'), 'x')
|
||||
self.assertEqual(jev_settings._strip_ide('ide/ide/x'), 'x')
|
||||
self.assertEqual(jev_settings._strip_ide('provider-ide/x'), 'provider-ide/x')
|
||||
|
||||
def test_prompts_model_criteria_and_thresholds_are_applied(self):
|
||||
self.save(model='jev-custom', questions={'ret': 'question A', 'sart': 'question B'}, thresholds={'ret': .9, 'sart': .8}, criteria={'ret': {'true': 'bad', 'false': 'good'}, 'sart': {'true': '', 'false': ''}})
|
||||
captured = []
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import copy
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import compression_settings as cs
|
||||
import gecis_context as gc
|
||||
|
||||
|
||||
class SummaryCoverage(unittest.TestCase):
|
||||
def test_long_noise_is_not_misclassified_as_one_giant_identifier(self):
|
||||
gc.require_summary_coverage('A' * 60000 + 'KARAR-4827' + 'B' * 60000,
|
||||
'KARAR-4827: original decision preserved.')
|
||||
|
||||
def test_partial_decision_ledger_is_rejected_and_full_ledger_is_accepted(self):
|
||||
records = [f'DEC-{i:04d}: src/feature_{i:04d}/config.json limit_{i:04d}={30000+i} TEST-QA-{i:04d} NO-DELETE-{i:04d}'
|
||||
for i in range(200)]
|
||||
source = [{'role': 'user', 'content': '\n'.join(records)}]
|
||||
with self.assertRaises(gc.TransferError):
|
||||
gc.require_summary_coverage(source, '\n'.join(records[:95]))
|
||||
gc.require_summary_coverage(source, '\n'.join(records))
|
||||
|
||||
def test_assignment_values_stay_associated_with_their_keys(self):
|
||||
source = [{'content': 'limit_a=123 limit_b=456 src/a.py DEC-1'}]
|
||||
self.assertGreater(gc.summary_missing(source, 'limit_a=456 limit_b=123 src/a.py DEC-1'), 0)
|
||||
gc.require_summary_coverage(source, 'DEC-1: `src/a.py`; limit_a: 123; limit_b: 456')
|
||||
|
||||
def test_secret_named_assignments_are_not_required_in_a_summary(self):
|
||||
gc.require_summary_coverage([{'content': 'api_key=123456 password=7890 otp=1234'}], 'Secrets omitted.')
|
||||
|
||||
def test_target_budget_is_not_multiplied_by_compression_level_again(self):
|
||||
settings = dict(cs.DEFAULTS, keepTurns=1, triggerPercent=60, targetPercent=25, level='fazla')
|
||||
req = {'input': [{'role': 'user', 'content': 'DEC-1 ' + 'x' * 60000},
|
||||
{'role': 'user', 'content': 'Keep this recent turn.'}]}
|
||||
observed = []
|
||||
def summarize(source, budget):
|
||||
observed.append(budget)
|
||||
return 'DEC-1: All accepted decisions, paths, settings and outstanding work are retained. ' * 2
|
||||
prepared = gc.Preparer().prepare(req, 32768, summarize, settings=settings, automatic=True)
|
||||
protected, _, recent = gc.split_history(req['input'], 1)
|
||||
available = gc.input_budget(req, 32768, settings, True) - gc.tokens(dict(req, input=protected + recent)) - 256
|
||||
self.assertEqual(observed, [available])
|
||||
self.assertLessEqual(gc.tokens(prepared), 32768 * .25)
|
||||
|
||||
def test_incomplete_old_checkpoint_is_rebuilt_without_overwriting_source(self):
|
||||
settings = dict(cs.DEFAULTS, keepTurns=1)
|
||||
req = {'input': [{'role': 'user', 'content': 'DEC-1 src/a.py limit_a=123 ' + 'x' * 60000},
|
||||
{'role': 'user', 'content': 'Latest'}]}
|
||||
original = copy.deepcopy(req)
|
||||
with tempfile.TemporaryDirectory() as home:
|
||||
store = cs.Checkpoints(home)
|
||||
old = gc.split_history(req['input'], 1)[1]
|
||||
store.save('scope', old, settings, 'Incomplete old summary without the required details. ' * 3)
|
||||
calls = []
|
||||
def summarize(source, budget):
|
||||
calls.append(1)
|
||||
return 'DEC-1 src/a.py limit_a=123. Accepted decision and all relevant constraints retained. ' * 2
|
||||
result = gc.Preparer().prepare(req, 32768, summarize, 'scope', settings=settings, checkpoints=store)
|
||||
self.assertEqual(calls, [1])
|
||||
self.assertEqual(req, original)
|
||||
gc.require_summary_coverage(old, gc.encoded(result))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -45,6 +45,29 @@ class ToolNamespaces(unittest.TestCase):
|
||||
self.assertEqual(restored["id"], "ctc_patch")
|
||||
self.assertEqual(restored["input"], "patch text")
|
||||
|
||||
def test_plaintext_peer_marker_survives_namespace_restore_and_split_frames(self):
|
||||
req = self.request(); names = {}; custom = env["flatten_request"](req, names)
|
||||
alias = req["tools"][0]["name"]
|
||||
arguments = json.dumps({"task_name": "child", "message": "Execute the complete delegated task."})
|
||||
item = {"type": "function_call", "name": alias, "id": "fc_peer", "call_id": "call_peer", "arguments": arguments}
|
||||
events = [
|
||||
{"type": "response.output_item.added", "item": dict(item, arguments="")},
|
||||
{"type": "response.function_call_arguments.delta", "item_id": "fc_peer", "delta": arguments[:17]},
|
||||
{"type": "response.function_call_arguments.delta", "item_id": "fc_peer", "delta": arguments[17:]},
|
||||
{"type": "response.output_item.done", "item": dict(item)},
|
||||
{"type": "response.completed", "response": {"output": [dict(item)]}},
|
||||
]
|
||||
raw = b"".join(("data: " + json.dumps(event) + "\n\n").encode() for event in events)
|
||||
adapter = env["SseRewriter"](custom, tool_names=names)
|
||||
output = b"".join(adapter.feed(raw[i:i+13]) for i in range(0, len(raw), 13)) + adapter.flush()
|
||||
restored = [json.loads(line[6:]) for line in output.decode().splitlines() if line.startswith("data: ")]
|
||||
self.assertNotIn("encrypted_function_args", restored[0]["item"])
|
||||
self.assertEqual(restored[1:3], events[1:3])
|
||||
for final in (restored[3]["item"], restored[4]["response"]["output"][0]):
|
||||
self.assertEqual(final["encrypted_function_args"], [])
|
||||
self.assertEqual((final["namespace"], final["name"]), ("collaboration", "spawn_agent"))
|
||||
self.assertEqual((final["id"], final["call_id"], final["arguments"]), ("fc_peer", "call_peer", arguments))
|
||||
|
||||
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, {})
|
||||
|
||||
@@ -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 = '21'
|
||||
export const CODEX_BRIDGE_VERSION = '24'
|
||||
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.42",
|
||||
"version": "1.0.0-beta.45",
|
||||
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
|
||||
"author": "AITURK / TurkServis",
|
||||
"repository": {
|
||||
|
||||
@@ -63,7 +63,8 @@ export function CompressionSettingsView() {
|
||||
{draft.budgetMode === 'percent' ? numeric('transferPercent', t('Hedef model bağlamı (%)', 'Target model context (%)'), 10, 90) : numeric('maxTokens', t('Maksimum giriş tokenı', 'Maximum input tokens'), 1024, 2000000)}
|
||||
<ListRow title={t('Otomatik sıkıştırma', 'Automatic compression')} action={<Switch aria-label={t('Otomatik sıkıştırma', 'Automatic compression')} disabled={busy} checked={draft.autoEnabled} onCheckedChange={v => change('autoEnabled', v)} />} />
|
||||
{numeric('triggerPercent', t('Başlama eşiği (%)', 'Trigger threshold (%)'), 20, 90)}
|
||||
{numeric('targetPercent', t('Sıkıştırma sonrası hedef (%)', 'Target after compression (%)'), 10, 80)}
|
||||
{numeric('targetPercent', t('Sıkıştırma sonrası üst sınır (%)', 'Maximum after compression (%)'), 10, 80)}
|
||||
<p className="text-sm text-(--ui-text-secondary)">{t('Bu bir üst sınırdır; tekrarlar azaldığında özet daha küçük olabilir. Sıkıştırma seviyesi anlatım yoğunluğunu belirler, bu sınırı ayrıca küçültmez.', 'This is an upper limit; removing repetition can produce a smaller summary. Compression level controls writing density without shrinking this budget again.')}</p>
|
||||
{numeric('timeoutSeconds', t('Sıkıştırıcı bekleme süresi (saniye)', 'Summarizer timeout (seconds)'), 1, 3600)}
|
||||
<Button disabled={busy || draft.targetPercent >= draft.triggerPercent} onClick={async () => {
|
||||
setBusy(true); setMessage(''); setFailed(false)
|
||||
|
||||
Generated
+1
-1
@@ -65,7 +65,7 @@
|
||||
},
|
||||
"apps/desktop": {
|
||||
"name": "aiturk-ide",
|
||||
"version": "1.0.0-beta.42",
|
||||
"version": "1.0.0-beta.45",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@assistant-ui/core": "0.2.23",
|
||||
|
||||
Reference in new issue
Block a user