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.
- package/.claude/rules/tfx-routing.md +14 -9
- package/README.ko.md +11 -9
- package/README.md +11 -8
- package/adapters/codex/skills/tfx-live/SKILL.md +30 -5
- package/bin/tfx-live.mjs +591 -83
- package/bin/triflux.mjs +9 -2
- package/hub/team/claude-transcript.mjs +27 -0
- package/hub/team/retry-state-machine.mjs +16 -24
- package/hub/team/session-context.mjs +13 -2
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +40 -40
- package/scripts/lib/codex-profile-config.mjs +44 -33
- package/scripts/lib/gemini-profiles.mjs +112 -126
- package/scripts/lib/model-family-resolver.mjs +335 -0
- package/scripts/setup.mjs +21 -47
- package/scripts/tfx-route.sh +29 -98
- package/skills/tfx-auto/SKILL.md +1 -1
- package/skills/tfx-lead/SKILL.md +5 -5
- package/skills/tfx-live/SKILL.md +30 -5
- package/skills/tfx-setup/SKILL.md +1 -1
|
@@ -1,72 +1,64 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
1
3
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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:
|
|
17
|
-
profiles:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
//
|
|
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: "
|
|
42
|
-
reviewer: "
|
|
43
|
-
"code-reviewer": "
|
|
44
|
-
"security-reviewer": "
|
|
45
|
-
"quality-reviewer": "
|
|
46
|
-
critic: "
|
|
47
|
-
architect: "
|
|
48
|
-
verifier: "
|
|
49
|
-
analyst: "
|
|
50
|
-
debugger: "
|
|
51
|
-
planner: "
|
|
52
|
-
scientist: "
|
|
53
|
-
"test-engineer": "
|
|
54
|
-
writer: "
|
|
55
|
-
executor: "
|
|
56
|
-
worker: "
|
|
57
|
-
"document-specialist": "
|
|
58
|
-
researcher: "
|
|
59
|
-
antigravity: "
|
|
60
|
-
agy: "
|
|
61
|
-
gemini: "
|
|
62
|
-
summarizer: "
|
|
63
|
-
classifier: "
|
|
64
|
-
triage: "
|
|
65
|
-
translator: "
|
|
66
|
-
formatter: "
|
|
67
|
-
explore: "
|
|
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 = "
|
|
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(
|
|
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
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
).
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
} catch {
|
|
130
|
+
let created = !existsSync(profilesPath);
|
|
131
|
+
if (!created) {
|
|
134
132
|
try {
|
|
135
|
-
|
|
136
|
-
} catch {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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,
|
|
161
|
-
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
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
|
+
}
|