diff --git a/apps/desktop/assets/codex-bridge/jev_settings.py b/apps/desktop/assets/codex-bridge/jev_settings.py index e014a9a..0a46539 100644 --- a/apps/desktop/assets/codex-bridge/jev_settings.py +++ b/apps/desktop/assets/codex-bridge/jev_settings.py @@ -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') diff --git a/apps/desktop/assets/codex-bridge/test_jev_settings.py b/apps/desktop/assets/codex-bridge/test_jev_settings.py index a876379..91b1409 100644 --- a/apps/desktop/assets/codex-bridge/test_jev_settings.py +++ b/apps/desktop/assets/codex-bridge/test_jev_settings.py @@ -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 = []