feat(codex-link): choose which models fill the Codex model menu and beta.41
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:
1 parent
81ee2ca646
commit
5c76c24c9c
17 files changed
+696
-8
No files matched your search
@@ -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 }
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in new issue
Block a user