feat(codex-link): choose which models fill the Codex model menu and beta.41
Server CI and Windows package / verify (push) Successful in 2m54s
Server CI and Windows package / windows-package (push) Successful in 4m50s

The Codex desktop app's model picker asks the app server for one page of
at most 100 models and never asks for the next page (a fixed limit in the
app). AITURK sent 262, so everything after the first 100 in alphabetical
provider order was out of reach from the menu (Gemini, most NanoGPT
uncensored models, Grok, ModelArk...).

- Bridge: bridge.json codex_liste {medya_gizle, efor_gizle, one_al, gizli},
  re-read when the file changes. Image/audio/video models and "-low/-high"
  copies whose base model is listed are left out by default; pinned models
  follow Codex's own models in the given order; hidden ones are removed.
  The candidate list with each model's place is written to
  log/son-liste/codex-adaylar.json for the settings screen.
- Settings > Codex Bağlama: "Models shown in Codex" with a live count of the
  100 slots, search, filter, pin/move up/hide per model and the two switches.
- The IDE keeps codex_liste when it rewrites bridge.json.
- Bridge version 21, IDE 1.0.0-beta.41.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
yilsemandClaude Opus 5.5 committed 2026-10-15 05:16:40 +03:00
1 parent 81ee2ca646
commit 5c76c24c9c
17 files changed
+696 -8

No files matched your search

+6 -3
View File
@@ -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 = '20'
export const CODEX_BRIDGE_VERSION = '21'
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'
@@ -359,8 +359,11 @@ export function serializeCodexBridgeConfig(previous?: null | string): string {
try {
const old = previous ? JSON.parse(previous) : null
if (old && typeof old === 'object' && old.aiturk_hat && typeof old.aiturk_hat === 'object') {
config.aiturk_hat = old.aiturk_hat
// The user's own choices from Settings → Codex Bağlama survive the IDE rewriting the file.
for (const key of ['aiturk_hat', 'codex_liste']) {
if (old && typeof old === 'object' && old[key] && typeof old[key] === 'object') {
config[key] = old[key]
}
}
} catch {
// An unreadable file has nothing worth keeping.
@@ -0,0 +1,50 @@
import { describe, expect, it } from 'vitest'
import { serializeCodexBridgeConfig } from './codex-link-config'
import { onizle, readAdaylar, readCodexListe, writeCodexListe } from './codex-liste-settings'
const aday = (slug: string, extra: Record<string, unknown> = {}) => ({ slug, ad: slug, kind: 'model', medya: false, efor: false, neden: '', sira: null, menude: false, ...extra })
describe('Codex model picker list setting', () => {
it('defaults to hiding media models and effort copies, with nothing pinned or hidden', () => {
expect(readCodexListe(null)).toEqual({ medya_gizle: true, efor_gizle: true, one_al: [], gizli: [] })
expect(readCodexListe('bozuk').medya_gizle).toBe(true)
expect(readCodexListe('{"codex_liste":{"efor_gizle":false,"one_al":["a","a",3]}}')).toEqual({ medya_gizle: true, efor_gizle: false, one_al: ['a'], gizli: [] })
})
it('writes only codex_liste, keeps the rest of bridge.json, and a pin wins over hiding', () => {
const raw = '{"port":8795,"aiturk_hat":{"secim":"onerilen"}}'
const out = JSON.parse(writeCodexListe(raw, { medya_gizle: false, efor_gizle: true, one_al: ['x'], gizli: ['x', 'y'] }))
expect(out).toEqual({ port: 8795, aiturk_hat: { secim: 'onerilen' }, codex_liste: { medya_gizle: false, efor_gizle: true, one_al: ['x'], gizli: ['y'] } })
expect(() => writeCodexListe('{bozuk', { one_al: [], gizli: [] })).toThrow()
expect(() => writeCodexListe(null, { one_al: 'x', gizli: [] })).toThrow()
})
it('reads the candidate list the bridge wrote and survives a missing or broken file', () => {
expect(readAdaylar(null)).toEqual({ adaylar: [], ozgunSayisi: 0, sinir: 100, at: null })
const st = readAdaylar(JSON.stringify({ sinir: 100, ozgun_sayisi: 9, at: 't', adaylar: [aday('ide/a', { neden: 'medya', medya: true }), { yanlis: 1 }] }))
expect(st.ozgunSayisi).toBe(9)
expect(st.adaylar).toHaveLength(1)
expect(st.adaylar[0].neden).toBe('medya')
})
it('previews the menu like the bridge: pins first, filters, and the 100 limit counts subscription models', () => {
const adaylar = [aday('ide/a'), aday('ide/b', { efor: true }), aday('ide/c', { medya: true }), ...Array.from({ length: 100 }, (_, i) => aday(`ide/m${i}`))]
const sonuc = onizle({ adaylar, ozgunSayisi: 2, sinir: 100 }, { medya_gizle: true, efor_gizle: true, one_al: ['ide/m99', 'ide/c'], gizli: ['ide/a'] })
const bul = (s: string) => sonuc.find(a => a.slug === s)!
expect(bul('ide/m99')).toMatchObject({ sira: 2, menude: true, neden: '' })
expect(bul('ide/c')).toMatchObject({ sira: 3, neden: '' }) // pinned media model is shown
expect(bul('ide/a').neden).toBe('gizli')
expect(bul('ide/b').neden).toBe('efor')
expect(sonuc.filter(a => a.menude)).toHaveLength(98)
expect(bul('ide/m98').menude).toBe(false)
})
it('survives the IDE rewriting bridge.json together with the work routing choice', () => {
const previous = JSON.stringify({ port: 1, aiturk_hat: { secim: 'kapali' }, codex_liste: { medya_gizle: false, efor_gizle: true, one_al: ['ide/x'], gizli: [] } })
const out = JSON.parse(serializeCodexBridgeConfig(previous))
expect(out.aiturk_hat).toEqual({ secim: 'kapali' })
expect(out.codex_liste).toEqual({ medya_gizle: false, efor_gizle: true, one_al: ['ide/x'], gizli: [] })
expect(JSON.parse(serializeCodexBridgeConfig('bozuk')).codex_liste).toBeUndefined()
})
})
@@ -0,0 +1,115 @@
import fs from 'node:fs'
import path from 'node:path'
import { type CodexAday, CODEX_LISTE_VARSAYILAN, CODEX_SECICI_SINIRI, type CodexListeSettings, type CodexListeState } from './codex-liste-types'
export type { CodexAday, CodexListeSettings, CodexListeState } from './codex-liste-types'
export { onizle } from './codex-liste-types'
const SINIR = CODEX_SECICI_SINIRI
const MAX_SLUGS = 2000
const VARSAYILAN: CodexListeSettings = CODEX_LISTE_VARSAYILAN
function slugs(value: unknown): string[] {
if (!Array.isArray(value)) return []
return [...new Set(value.filter((s): s is string => typeof s === 'string' && s.trim() !== ''))].slice(0, MAX_SLUGS)
}
export function readCodexListe(raw: null | string): CodexListeSettings {
try {
const liste = raw ? JSON.parse(raw)?.codex_liste : null
if (!liste || typeof liste !== 'object') return { ...VARSAYILAN, one_al: [], gizli: [] }
return {
medya_gizle: liste.medya_gizle !== false,
efor_gizle: liste.efor_gizle !== false,
one_al: slugs(liste.one_al),
gizli: slugs(liste.gizli)
}
} catch {
return { ...VARSAYILAN, one_al: [], gizli: [] }
}
}
export function writeCodexListe(raw: null | string, value: unknown): string {
const v = value as Partial<CodexListeSettings> | null
if (!v || typeof v !== 'object') throw new Error('Invalid setting')
if (!Array.isArray(v.one_al) || !Array.isArray(v.gizli)) throw new Error('Invalid model list')
let config: Record<string, unknown> = {}
try {
const parsed = raw ? JSON.parse(raw) : {}
config = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
} catch {
throw new Error('bridge.json is not valid JSON')
}
const liste: CodexListeSettings = {
medya_gizle: v.medya_gizle !== false,
efor_gizle: v.efor_gizle !== false,
one_al: slugs(v.one_al),
gizli: slugs(v.gizli).filter(s => !slugs(v.one_al).includes(s))
}
return `${JSON.stringify({ ...config, codex_liste: liste }, null, 2)}\n`
}
export function readAdaylar(raw: null | string): Omit<CodexListeState, 'ayar'> {
const bos = { adaylar: [], ozgunSayisi: 0, sinir: SINIR, at: null }
try {
const veri = raw ? JSON.parse(raw) : null
if (!veri || !Array.isArray(veri.adaylar)) return bos
const adaylar: CodexAday[] = veri.adaylar
.filter((a: unknown): a is Record<string, unknown> => Boolean(a) && typeof (a as { slug?: unknown }).slug === 'string')
.map((a: Record<string, unknown>) => ({
slug: String(a.slug),
ad: typeof a.ad === 'string' ? a.ad : String(a.slug),
kind: typeof a.kind === 'string' ? a.kind : 'model',
medya: a.medya === true,
efor: a.efor === true,
neden: (['efor', 'gizli', 'medya'].includes(String(a.neden)) ? a.neden : '') as CodexAday['neden'],
sira: typeof a.sira === 'number' ? a.sira : null,
menude: a.menude === true
}))
return {
adaylar,
ozgunSayisi: typeof veri.ozgun_sayisi === 'number' ? veri.ozgun_sayisi : 0,
sinir: typeof veri.sinir === 'number' ? veri.sinir : SINIR,
at: typeof veri.at === 'string' ? veri.at : null
}
} catch {
return bos
}
}
export function createCodexListeStore(directory: string) {
const file = path.join(directory, 'bridge.json')
const adayFile = path.join(directory, 'log', 'son-liste', 'codex-adaylar.json')
const read = (f: string) => (fs.existsSync(f) ? fs.readFileSync(f, 'utf8') : null)
const state = (raw: null | string): CodexListeState => ({ ayar: readCodexListe(raw), ...readAdaylar(read(adayFile)) })
return {
get: (): CodexListeState => state(read(file)),
save(value: unknown): CodexListeState {
const text = writeCodexListe(read(file), value)
fs.mkdirSync(directory, { recursive: true })
const tmp = `${file}.${process.pid}.tmp`
try {
fs.writeFileSync(tmp, text, 'utf8')
fs.renameSync(tmp, file)
} finally {
if (fs.existsSync(tmp)) fs.unlinkSync(tmp)
}
return state(text)
}
}
}
@@ -0,0 +1,62 @@
/** Pure part of the Codex model-list setting (no Node imports), shared by the main process and the
* settings screen. */
/** Which added models Codex's model picker shows. The Codex desktop app asks for one page of at most
* 100 models and never asks for the next page, so the user chooses which models fill it. Stored in the
* Codex bridge's `bridge.json` under `codex_liste`; the bridge re-reads it when the file changes and writes
* the candidate list it built to `log/son-liste/codex-adaylar.json`. */
export interface CodexListeSettings {
medya_gizle: boolean
efor_gizle: boolean
one_al: string[]
gizli: string[]
}
export interface CodexAday {
slug: string
ad: string
kind: string
medya: boolean
efor: boolean
neden: '' | 'efor' | 'gizli' | 'medya'
sira: null | number
menude: boolean
}
export interface CodexListeState {
ayar: CodexListeSettings
adaylar: CodexAday[]
ozgunSayisi: number
sinir: number
at: null | string
}
export const CODEX_SECICI_SINIRI = 100
export const CODEX_LISTE_VARSAYILAN: CodexListeSettings = { medya_gizle: true, efor_gizle: true, one_al: [], gizli: [] }
/** The menu the setting produces, computed the way the bridge does it, so the screen can show the effect
* before the bridge rebuilds the list. Candidates arrive in the bridge's base order. */
export function onizle(state: Pick<CodexListeState, 'adaylar' | 'ozgunSayisi' | 'sinir'>, ayar: CodexListeSettings) {
const one = ayar.one_al.filter(s => state.adaylar.some(a => a.slug === s))
const gizli = new Set(ayar.gizli)
const neden = (a: CodexAday): CodexAday['neden'] => {
if (gizli.has(a.slug)) return 'gizli'
if (one.includes(a.slug)) return ''
if (a.medya && ayar.medya_gizle) return 'medya'
if (a.efor && ayar.efor_gizle) return 'efor'
return ''
}
const kalan = state.adaylar.filter(a => !neden(a))
const sirali = [...one.map(s => kalan.find(a => a.slug === s)).filter((a): a is CodexAday => Boolean(a)), ...kalan.filter(a => !one.includes(a.slug))]
const yer = new Map(sirali.map((a, i) => [a.slug, state.ozgunSayisi + i]))
return state.adaylar.map(a => {
const n = neden(a)
const sira = n ? null : (yer.get(a.slug) ?? null)
return { ...a, neden: n, sira, menude: sira !== null && sira < state.sinir }
})
}
+12
View File
@@ -380,6 +380,7 @@ import { collectSshConfigHosts, parseSshGOutput } from './ssh-config'
import { createSshProbeConnection, pickLocalPort, redactSecrets, SshConnection } from './ssh-connection'
import { createStagedTransitionStore } from './staged-transition'
import { createCompressionStore } from './compression-settings'
import { createCodexListeStore } from './codex-liste-settings'
import { createIsHattiStore } from './is-hatti-settings'
import { createStreamThrottle } from './stream-throttle'
import { registerTerminalIpc } from './terminal-ipc'
@@ -17197,6 +17198,7 @@ ipcMain.handle('hermes:quick-entry:settings:set', async (_event, patch) => {
const AITURK_IDE_DATA_ROOT = path.dirname(HERMES_HOME)
const compressionStore = createCompressionStore(path.join(AITURK_IDE_DATA_ROOT, 'codex-bridge'))
const isHattiStore = createIsHattiStore(path.join(AITURK_IDE_DATA_ROOT, 'codex-bridge'))
const codexListeStore = createCodexListeStore(path.join(AITURK_IDE_DATA_ROOT, 'codex-bridge'))
const stagedTransition = createStagedTransitionStore(
path.join(AITURK_IDE_DATA_ROOT, 'codex-bridge'),
path.join(app.getPath('home'), '.typesafe', 'typesafe.env'),
@@ -17319,6 +17321,16 @@ ipcMain.handle('aiturk:is-hatti:set', async (event, settings) => {
return isHattiStore.save(settings)
})
ipcMain.handle('aiturk:codex-liste:get', async event => {
assertCodexLinkSender(event)
return codexListeStore.get()
})
ipcMain.handle('aiturk:codex-liste:set', async (event, settings) => {
assertCodexLinkSender(event)
return codexListeStore.save(settings)
})
ipcMain.handle('aiturk:staged-transition:set', async (event, patch) => {
assertCodexLinkSender(event)
stagedTransition.save(patch)
+2
View File
@@ -299,6 +299,8 @@ contextBridge.exposeInMainWorld('hermesDesktop', {
setCompression: settings => ipcRenderer.invoke('aiturk:compression:set', settings),
getIsHatti: () => ipcRenderer.invoke('aiturk:is-hatti:get'),
setIsHatti: settings => ipcRenderer.invoke('aiturk:is-hatti:set', settings),
getCodexListe: () => ipcRenderer.invoke('aiturk:codex-liste:get'),
setCodexListe: settings => ipcRenderer.invoke('aiturk:codex-liste:set', settings),
getStagedTransition: () => ipcRenderer.invoke('aiturk:staged-transition:get'),
setStagedTransition: patch => ipcRenderer.invoke('aiturk:staged-transition:set', patch),
get: () => ipcRenderer.invoke('aiturk:codex-link:get'),