triflux 10.61.2 → 10.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,72 +1,64 @@
1
+ import { copyFileSync, existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
1
3
  import {
2
- copyFileSync,
3
- existsSync,
4
- mkdirSync,
5
- readFileSync,
6
- writeFileSync,
7
- } from "node:fs";
8
- import { homedir } from "node:os";
9
- import { join } from "node:path";
4
+ FLASH_FAMILY_PROFILES,
5
+ modelFamilyPaths,
6
+ resolveModelFamilies,
7
+ writeModelFile,
8
+ } from "./model-family-resolver.mjs";
10
9
 
11
- // 모델명 SSOT. 값은 `agy models` 가 출력하는 display name 형식이다
12
- // (`--model` 인자가 이 문자열을 받는다). 옛 `gemini-*-preview` ID 형식이 아님.
13
- // 카탈로그 확인: agy 1.1.27 `agy models` (2026-09-07). 운영 정책은 3.8 Flash 한 세대만
14
- // 쓰고 effort(Low/Medium/High)로 차등한다. Pro 와 이전 세대 Flash 는 기본 프로필에서 뺐다.
15
10
  const DEFAULT_GEMINI_PROFILES = {
16
- model: "Gemini 3.8 Flash (Medium)",
17
- profiles: {
18
- flash38_low: {
19
- model: "Gemini 3.8 Flash (Low)",
20
- hint: "3.8 Flash (Low) — 경량·저비용",
21
- },
22
- flash38: {
23
- model: "Gemini 3.8 Flash (Medium)",
24
- hint: "3.8 Flash (Medium) — 기본, 비용·속도 균형",
25
- },
26
- flash38_high: {
27
- model: "Gemini 3.8 Flash (High)",
28
- hint: "3.8 Flash (High) — 코드/추론 강화",
29
- },
30
- },
11
+ model: FLASH_FAMILY_PROFILES.flash.model,
12
+ profiles: Object.fromEntries(
13
+ Object.entries(FLASH_FAMILY_PROFILES).map(([name, { model }]) => [
14
+ name,
15
+ { model },
16
+ ]),
17
+ ),
31
18
  };
32
19
 
20
+ export function normalizeGeminiProfileName(name) {
21
+ const raw = String(name ?? "").trim();
22
+ return raw.replace(/^flash\d*(?=_(?:low|high)$|$)/, "flash");
23
+ }
24
+
33
25
  // ── 용도별 effort (SSOT) ──
34
- // 한 세대(3.8 Flash)만 쓰고 역할에 따라 effort 를 나눈다. 키는 tfx 에이전트/역할 이름
26
+ // Flash 가족 안에서 역할에 따라 effort 를 나눈다. 키는 tfx 에이전트/역할 이름
35
27
  // (agent-map.json 의 에이전트, tfx multi --assign 의 role). bash(tfx-route.sh)와
36
28
  // 실행 모드(execution-mode)가 이 표를 읽는다.
37
29
  // High : 판단이 결과를 좌우하는 역할 (검토, 설계, 디버깅, 분석)
38
30
  // Medium : 생성/작성/실행 (기본값)
39
31
  // Low : 분류, 요약, 추출, 번역처럼 짧고 기계적인 작업
40
32
  const GEMINI_PROFILE_BY_PURPOSE = Object.freeze({
41
- designer: "flash38_high",
42
- reviewer: "flash38_high",
43
- "code-reviewer": "flash38_high",
44
- "security-reviewer": "flash38_high",
45
- "quality-reviewer": "flash38_high",
46
- critic: "flash38_high",
47
- architect: "flash38_high",
48
- verifier: "flash38_high",
49
- analyst: "flash38_high",
50
- debugger: "flash38_high",
51
- planner: "flash38_high",
52
- scientist: "flash38_high",
53
- "test-engineer": "flash38_high",
54
- writer: "flash38",
55
- executor: "flash38",
56
- worker: "flash38",
57
- "document-specialist": "flash38",
58
- researcher: "flash38",
59
- antigravity: "flash38",
60
- agy: "flash38",
61
- gemini: "flash38",
62
- summarizer: "flash38_low",
63
- classifier: "flash38_low",
64
- triage: "flash38_low",
65
- translator: "flash38_low",
66
- formatter: "flash38_low",
67
- explore: "flash38_low",
33
+ designer: "flash_high",
34
+ reviewer: "flash_high",
35
+ "code-reviewer": "flash_high",
36
+ "security-reviewer": "flash_high",
37
+ "quality-reviewer": "flash_high",
38
+ critic: "flash_high",
39
+ architect: "flash_high",
40
+ verifier: "flash_high",
41
+ analyst: "flash_high",
42
+ debugger: "flash_high",
43
+ planner: "flash_high",
44
+ scientist: "flash_high",
45
+ "test-engineer": "flash_high",
46
+ writer: "flash",
47
+ executor: "flash",
48
+ worker: "flash",
49
+ "document-specialist": "flash",
50
+ researcher: "flash",
51
+ antigravity: "flash",
52
+ agy: "flash",
53
+ gemini: "flash",
54
+ summarizer: "flash_low",
55
+ classifier: "flash_low",
56
+ triage: "flash_low",
57
+ translator: "flash_low",
58
+ formatter: "flash_low",
59
+ explore: "flash_low",
68
60
  });
69
- const GEMINI_DEFAULT_PURPOSE_PROFILE = "flash38";
61
+ const GEMINI_DEFAULT_PURPOSE_PROFILE = "flash";
70
62
 
71
63
  function resolveGeminiProfileForPurpose(name) {
72
64
  const key = String(name ?? "")
@@ -84,70 +76,66 @@ function resolveGeminiModel(profileOrModel, opts = {}) {
84
76
  const profilesPath =
85
77
  opts.profilesPath ||
86
78
  process.env.GEMINI_PROFILES_PATH ||
87
- join(homedir(), ".gemini", "triflux-profiles.json");
79
+ join(modelFamilyPaths(opts).home, ".gemini", "triflux-profiles.json");
88
80
  let cfg = null;
89
81
  try {
90
82
  cfg = JSON.parse(readFileSync(profilesPath, "utf8"));
91
83
  } catch {
92
84
  cfg = null;
93
85
  }
94
- const entry = cfg?.profiles?.[raw];
95
- const fromFile = typeof entry === "string" ? entry : entry?.model;
96
- if (typeof fromFile === "string" && fromFile.trim()) return fromFile.trim();
97
- const fromDefault = DEFAULT_GEMINI_PROFILES.profiles[raw];
98
- if (fromDefault) return fromDefault.model;
99
- if (typeof cfg?.model === "string" && cfg.model.trim())
100
- return cfg.model.trim();
101
- return DEFAULT_GEMINI_PROFILES.model;
86
+ const name = normalizeGeminiProfileName(raw);
87
+ const modelValue = (entry) =>
88
+ typeof entry === "string"
89
+ ? entry
90
+ : typeof entry?.model === "string"
91
+ ? entry.model
92
+ : entry?.model?.name || entry?.name;
93
+ for (const bucket of [
94
+ cfg?.profiles,
95
+ cfg?.model?.profiles,
96
+ cfg?.modelProfiles,
97
+ cfg?.models,
98
+ ]) {
99
+ const entry = bucket?.[name] || bucket?.[raw];
100
+ const value = modelValue(entry);
101
+ if (typeof value === "string" && value.trim()) return value.trim();
102
+ }
103
+ if (name !== "default" && DEFAULT_GEMINI_PROFILES.profiles[name])
104
+ return DEFAULT_GEMINI_PROFILES.profiles[name].model;
105
+ const fallback =
106
+ cfg?.defaultModel ||
107
+ cfg?.default_profile ||
108
+ cfg?.defaultProfile ||
109
+ (typeof cfg?.model === "string" ? cfg.model : cfg?.model?.default);
110
+ if (typeof fallback === "string" && fallback.trim()) return fallback.trim();
111
+ const defaultProfile = normalizeGeminiProfileName(
112
+ (opts.env || process.env).TFX_GEMINI_DEFAULT_PROFILE,
113
+ );
114
+ return (
115
+ modelValue(cfg?.profiles?.[defaultProfile]) ||
116
+ DEFAULT_GEMINI_PROFILES.profiles[defaultProfile]?.model ||
117
+ DEFAULT_GEMINI_PROFILES.model
118
+ );
102
119
  }
103
120
 
104
- const DEFAULT_PROFILE_COUNT = Object.keys(
105
- DEFAULT_GEMINI_PROFILES.profiles,
106
- ).length;
107
-
108
- function ensureGeminiProfiles({
109
- geminiDir = join(homedir(), ".gemini"),
110
- profilesPath = join(geminiDir, "triflux-profiles.json"),
111
- } = {}) {
121
+ function ensureGeminiProfiles(opts = {}) {
122
+ const geminiDir =
123
+ opts.geminiDir || join(modelFamilyPaths(opts).home, ".gemini");
124
+ const profilesPath =
125
+ opts.profilesPath ||
126
+ process.env.GEMINI_PROFILES_PATH ||
127
+ join(geminiDir, "triflux-profiles.json");
112
128
  try {
113
- if (!existsSync(geminiDir)) mkdirSync(geminiDir, { recursive: true });
114
-
115
- if (!existsSync(profilesPath)) {
116
- writeFileSync(
117
- profilesPath,
118
- JSON.stringify(DEFAULT_GEMINI_PROFILES, null, 2) + "\n",
119
- { encoding: "utf8", mode: 0o600 },
120
- );
121
- return {
122
- ok: true,
123
- created: true,
124
- added: DEFAULT_PROFILE_COUNT,
125
- count: DEFAULT_PROFILE_COUNT,
126
- message: null,
127
- };
128
- }
129
-
130
129
  let cfg;
131
- try {
132
- cfg = JSON.parse(readFileSync(profilesPath, "utf8"));
133
- } catch {
130
+ let created = !existsSync(profilesPath);
131
+ if (!created) {
134
132
  try {
135
- copyFileSync(profilesPath, profilesPath + `.bak.${Date.now()}`);
136
- } catch {}
137
- writeFileSync(
138
- profilesPath,
139
- JSON.stringify(DEFAULT_GEMINI_PROFILES, null, 2) + "\n",
140
- { encoding: "utf8", mode: 0o600 },
141
- );
142
- return {
143
- ok: true,
144
- created: true,
145
- added: DEFAULT_PROFILE_COUNT,
146
- count: DEFAULT_PROFILE_COUNT,
147
- message: null,
148
- };
133
+ cfg = JSON.parse(readFileSync(profilesPath, "utf8"));
134
+ } catch {
135
+ copyFileSync(profilesPath, `${profilesPath}.bak.${Date.now()}`);
136
+ created = true;
137
+ }
149
138
  }
150
-
151
139
  if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) cfg = {};
152
140
  if (
153
141
  !cfg.profiles ||
@@ -155,28 +143,25 @@ function ensureGeminiProfiles({
155
143
  Array.isArray(cfg.profiles)
156
144
  )
157
145
  cfg.profiles = {};
158
-
146
+ const { profiles } = resolveModelFamilies("agy", {
147
+ ...opts,
148
+ home: opts.home || dirname(geminiDir),
149
+ });
159
150
  let added = 0;
160
- for (const [name, value] of Object.entries(
161
- DEFAULT_GEMINI_PROFILES.profiles,
162
- )) {
163
- if (!cfg.profiles[name]) {
164
- cfg.profiles[name] = value;
165
- added++;
166
- }
151
+ for (const [name, { model }] of Object.entries(profiles)) {
152
+ if (!cfg.profiles[name]) added++;
153
+ cfg.profiles[name] = { model };
167
154
  }
168
- if (!cfg.model) cfg.model = DEFAULT_GEMINI_PROFILES.model;
169
-
170
- if (added > 0) {
171
- writeFileSync(profilesPath, JSON.stringify(cfg, null, 2) + "\n", {
172
- encoding: "utf8",
173
- mode: 0o600,
174
- });
175
- }
176
-
155
+ if (!cfg.model || /^Gemini \d+(?:\.\d+)* Flash \(/.test(cfg.model))
156
+ cfg.model = profiles.flash.model;
157
+ const changed = writeModelFile(
158
+ profilesPath,
159
+ `${JSON.stringify(cfg, null, 2)}\n`,
160
+ );
177
161
  return {
178
162
  ok: true,
179
- created: false,
163
+ created,
164
+ changed,
180
165
  added,
181
166
  count: Object.keys(cfg.profiles).length,
182
167
  message: null,
@@ -185,6 +170,7 @@ function ensureGeminiProfiles({
185
170
  return {
186
171
  ok: false,
187
172
  created: false,
173
+ changed: false,
188
174
  added: 0,
189
175
  count: 0,
190
176
  message: error.message,
@@ -0,0 +1,335 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import {
3
+ mkdirSync,
4
+ readFileSync,
5
+ renameSync,
6
+ rmSync,
7
+ statSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import { resolveTrifluxHome } from "./machine-profile.mjs";
12
+
13
+ export const CODEX_FAMILY_PROFILES = Object.freeze({
14
+ sol_high: { family: "sol", effort: "high", model: "gpt-6.1-sol" },
15
+ sol_med: { family: "sol", effort: "medium", model: "gpt-6.1-sol" },
16
+ astra_xhigh: { family: "astra", effort: "xhigh", model: "gpt-6-astra" },
17
+ astra_max: { family: "astra", effort: "max", model: "gpt-6-astra" },
18
+ astra_ultra: { family: "astra", effort: "ultra", model: "gpt-6-astra" },
19
+ luna_high: { family: "luna", effort: "high", model: "gpt-6-luna" },
20
+ luna_low: { family: "luna", effort: "low", model: "gpt-6-luna" },
21
+ });
22
+ export const FLASH_FAMILY_PROFILES = Object.freeze({
23
+ flash_low: {
24
+ family: "flash",
25
+ effort: "low",
26
+ model: "Gemini 3.8 Flash (Low)",
27
+ },
28
+ flash: {
29
+ family: "flash",
30
+ effort: "medium",
31
+ model: "Gemini 3.8 Flash (Medium)",
32
+ },
33
+ flash_high: {
34
+ family: "flash",
35
+ effort: "high",
36
+ model: "Gemini 3.8 Flash (High)",
37
+ },
38
+ });
39
+ const DAY_MS = 86_400_000;
40
+ const CODEX_SLUG = /^gpt-(\d+(?:\.\d+)*)-(sol|astra|luna)$/;
41
+ const FLASH_SLUG = /^gemini-(\d+(?:\.\d+)*)-flash-(low|medium|high)$/;
42
+
43
+ function compareVersions(a, b) {
44
+ const left = a.split(".").map(Number);
45
+ const right = b.split(".").map(Number);
46
+ for (let i = 0; i < Math.max(left.length, right.length); i++) {
47
+ const difference = (left[i] || 0) - (right[i] || 0);
48
+ if (difference) return difference;
49
+ }
50
+ return 0;
51
+ }
52
+
53
+ export function selectCodexModel(models, family, effort) {
54
+ return (
55
+ (models || [])
56
+ .filter((item) => {
57
+ const match = CODEX_SLUG.exec(item?.slug);
58
+ return (
59
+ match?.[2] === family &&
60
+ item.visibility === "list" &&
61
+ Array.isArray(item.supported_reasoning_levels) &&
62
+ item.supported_reasoning_levels.some(
63
+ (level) => (level?.effort ?? level) === effort,
64
+ )
65
+ );
66
+ })
67
+ .sort((a, b) =>
68
+ compareVersions(CODEX_SLUG.exec(b.slug)[1], CODEX_SLUG.exec(a.slug)[1]),
69
+ )[0] || null
70
+ );
71
+ }
72
+
73
+ export function parseAgyModels(output) {
74
+ return String(output)
75
+ .split(/\r?\n/)
76
+ .flatMap((line) => {
77
+ const [slug, model] = line.trim().split("\t");
78
+ return FLASH_SLUG.test(slug) && model?.trim()
79
+ ? [{ slug, model: model.trim() }]
80
+ : [];
81
+ });
82
+ }
83
+
84
+ export function selectFlashModel(models, effort) {
85
+ return (
86
+ (models || [])
87
+ .filter(
88
+ (item) =>
89
+ FLASH_SLUG.exec(item?.slug)?.[2] === effort &&
90
+ typeof item.model === "string",
91
+ )
92
+ .sort((a, b) =>
93
+ compareVersions(FLASH_SLUG.exec(b.slug)[1], FLASH_SLUG.exec(a.slug)[1]),
94
+ )[0] || null
95
+ );
96
+ }
97
+
98
+ export function modelFamilyPaths(opts = {}) {
99
+ const env = opts.env || process.env;
100
+ const platform = opts.platform || process.platform;
101
+ const home = opts.home || resolveTrifluxHome({ env, platform });
102
+ const stateDir =
103
+ opts.stateDir ||
104
+ join(
105
+ platform === "win32"
106
+ ? env.APPDATA || join(home, "AppData", "Roaming")
107
+ : env.XDG_CONFIG_HOME || join(home, ".config"),
108
+ "triflux",
109
+ );
110
+ return {
111
+ home,
112
+ stateDir,
113
+ codexHome:
114
+ opts.codexHome ||
115
+ env.CODEX_HOME ||
116
+ env.TFX_CODEX_HOME ||
117
+ join(home, ".codex"),
118
+ pinsPath: join(stateDir, "models.json"),
119
+ };
120
+ }
121
+
122
+ function readJson(path) {
123
+ try {
124
+ const value = JSON.parse(readFileSync(path, "utf8"));
125
+ return value && typeof value === "object" && !Array.isArray(value)
126
+ ? value
127
+ : null;
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ function signature(path) {
134
+ try {
135
+ const stat = statSync(path);
136
+ return `${path}:${stat.ino}:${stat.mtimeMs}:${stat.size}`;
137
+ } catch {
138
+ return `${path}:missing`;
139
+ }
140
+ }
141
+
142
+ export function writeModelFile(path, content) {
143
+ try {
144
+ if (readFileSync(path, "utf8") === content) return false;
145
+ } catch {}
146
+ mkdirSync(dirname(path), { recursive: true });
147
+ const temp = `${path}.${process.pid}.tmp`;
148
+ writeFileSync(temp, content, { encoding: "utf8", mode: 0o600 });
149
+ renameSync(temp, path);
150
+ return true;
151
+ }
152
+
153
+ function saveJson(path, value) {
154
+ try {
155
+ writeModelFile(path, `${JSON.stringify(value)}\n`);
156
+ } catch {
157
+ // 읽기 전용 상태 디렉터리여도 이번 실행의 해석 결과는 쓸 수 있다.
158
+ }
159
+ }
160
+
161
+ const codexCatalogs = new Map();
162
+ export function readCodexModelCatalog(opts = {}) {
163
+ const path = join(modelFamilyPaths(opts).codexHome, "models_cache.json");
164
+ const key = signature(path);
165
+ let cached = codexCatalogs.get(path);
166
+ if (cached?.key !== key) {
167
+ const value = readJson(path);
168
+ cached = { key, catalog: Array.isArray(value?.models) ? value : null };
169
+ codexCatalogs.set(path, cached);
170
+ }
171
+ return cached.catalog;
172
+ }
173
+
174
+ export function resolveModelFamilies(provider, opts = {}) {
175
+ const paths = modelFamilyPaths(opts);
176
+ const definitions =
177
+ provider === "codex" ? CODEX_FAMILY_PROFILES : FLASH_FAMILY_PROFILES;
178
+ const recordPath = join(paths.stateDir, `${provider}-model-resolution.json`);
179
+ const previous = readJson(recordPath) || {};
180
+ const now = (opts.now || Date.now)();
181
+ const fetchedAt = new Date(now).toISOString();
182
+ const run =
183
+ opts.runCommand ||
184
+ ((command, args) =>
185
+ execFileSync(command, args, {
186
+ encoding: "utf8",
187
+ timeout: 10_000,
188
+ stdio: ["ignore", "pipe", "ignore"],
189
+ env: {
190
+ ...(opts.env || process.env),
191
+ HOME: paths.home,
192
+ CODEX_HOME: paths.codexHome,
193
+ },
194
+ maxBuffer: 4 * 1024 * 1024,
195
+ }));
196
+ let catalog = null;
197
+ let sourceSignature;
198
+ let checkedAt = previous.checked_at;
199
+ if (provider === "codex") {
200
+ const cachePath = join(paths.codexHome, "models_cache.json");
201
+ sourceSignature = signature(cachePath);
202
+ if (sourceSignature !== previous.sourceSignature)
203
+ catalog = readCodexModelCatalog(opts);
204
+ if (
205
+ sourceSignature.endsWith(":missing") &&
206
+ opts.allowCommand !== false &&
207
+ (!checkedAt || now - Date.parse(checkedAt) >= DAY_MS)
208
+ ) {
209
+ checkedAt = fetchedAt;
210
+ try {
211
+ const parsed = JSON.parse(
212
+ run(opts.command || "codex", ["debug", "models"]),
213
+ );
214
+ if (Array.isArray(parsed?.models))
215
+ catalog = { ...parsed, fetched_at: parsed.fetched_at || fetchedAt };
216
+ } catch {}
217
+ }
218
+ } else {
219
+ const cachePath = join(paths.stateDir, "agy-models.json");
220
+ let cached = readJson(cachePath) || {};
221
+ if (
222
+ opts.allowCommand !== false &&
223
+ (!cached.checked_at || now - Date.parse(cached.checked_at) >= DAY_MS)
224
+ ) {
225
+ const lock = `${cachePath}.lock`;
226
+ let acquired = false;
227
+ try {
228
+ if (opts.persist !== false) {
229
+ mkdirSync(paths.stateDir, { recursive: true });
230
+ // 프로세스가 끊긴 잠금만 회수한다. CLI 조회 제한은 10초다.
231
+ try {
232
+ if (now - statSync(lock).mtimeMs > 60_000)
233
+ rmSync(lock, { recursive: true, force: true });
234
+ } catch {}
235
+ mkdirSync(lock);
236
+ acquired = true;
237
+ cached = readJson(cachePath) || cached;
238
+ }
239
+ if (
240
+ !cached.checked_at ||
241
+ now - Date.parse(cached.checked_at) >= DAY_MS
242
+ ) {
243
+ cached.checked_at = fetchedAt;
244
+ try {
245
+ const models = parseAgyModels(
246
+ run(opts.command || "agy", ["models"]),
247
+ );
248
+ if (models.length)
249
+ cached = { models, fetched_at: fetchedAt, checked_at: fetchedAt };
250
+ } catch {}
251
+ if (opts.persist !== false) saveJson(cachePath, cached);
252
+ }
253
+ } catch {
254
+ // 다른 디스패치가 갱신 중이면 기존 결과로 실행한다.
255
+ } finally {
256
+ if (acquired) rmSync(lock, { recursive: true, force: true });
257
+ }
258
+ }
259
+ sourceSignature = signature(cachePath);
260
+ checkedAt = cached.checked_at;
261
+ if (Array.isArray(cached.models)) catalog = cached;
262
+ }
263
+ const profiles = {};
264
+ const selectedProfiles = {};
265
+ const pins = readJson(paths.pinsPath) || {};
266
+ for (const [name, definition] of Object.entries(definitions)) {
267
+ const saved = previous.profiles?.[name];
268
+ const last =
269
+ typeof saved?.model === "string" && saved.effort === definition.effort
270
+ ? saved
271
+ : null;
272
+ const candidate =
273
+ provider === "codex"
274
+ ? selectCodexModel(
275
+ catalog?.models,
276
+ definition.family,
277
+ definition.effort,
278
+ )
279
+ : selectFlashModel(catalog?.models, definition.effort);
280
+ profiles[name] =
281
+ candidate &&
282
+ (!last?.fetched_at ||
283
+ !catalog.fetched_at ||
284
+ Date.parse(catalog.fetched_at) >= Date.parse(last.fetched_at))
285
+ ? {
286
+ model: provider === "codex" ? candidate.slug : candidate.model,
287
+ effort: definition.effort,
288
+ fetched_at: catalog.fetched_at || fetchedAt,
289
+ }
290
+ : last || {
291
+ model: definition.model,
292
+ effort: definition.effort,
293
+ fetched_at: null,
294
+ };
295
+ selectedProfiles[name] = { ...profiles[name] };
296
+ const pin = pins[definition.family];
297
+ if (
298
+ typeof pin === "string" &&
299
+ provider === "codex" &&
300
+ CODEX_SLUG.exec(pin)?.[2] === definition.family
301
+ ) {
302
+ selectedProfiles[name] = {
303
+ model: pin,
304
+ effort: definition.effort,
305
+ source: paths.pinsPath,
306
+ fetched_at: null,
307
+ };
308
+ } else if (
309
+ typeof pin === "string" &&
310
+ provider === "agy" &&
311
+ /^gemini-\d+(?:\.\d+)*-flash$/.test(pin)
312
+ ) {
313
+ const slug = `${pin}-${definition.effort}`;
314
+ const version = pin.slice(7, -6);
315
+ const effort =
316
+ definition.effort[0].toUpperCase() + definition.effort.slice(1);
317
+ selectedProfiles[name] = {
318
+ model:
319
+ catalog?.models?.find((item) => item.slug === slug)?.model ||
320
+ `Gemini ${version} Flash (${effort})`,
321
+ effort: definition.effort,
322
+ source: paths.pinsPath,
323
+ fetched_at: null,
324
+ };
325
+ }
326
+ }
327
+ const record = {
328
+ sourceSignature,
329
+ checked_at: checkedAt || null,
330
+ profiles,
331
+ selectedProfiles,
332
+ };
333
+ if (opts.persist !== false) saveJson(recordPath, record);
334
+ return { profiles: selectedProfiles };
335
+ }