@triflux/remote 10.61.1 → 10.62.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/hub/team/retry-state-machine.mjs +16 -24
- package/hub/team/session-context.mjs +3 -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
|
@@ -23,10 +23,8 @@ import {
|
|
|
23
23
|
} from "node:fs";
|
|
24
24
|
import { dirname, join } from "node:path";
|
|
25
25
|
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
normalizeCodexProfileName,
|
|
29
|
-
} from "../../scripts/lib/codex-profile-config.mjs";
|
|
26
|
+
import { normalizeCodexProfileName } from "../../scripts/lib/codex-profile-config.mjs";
|
|
27
|
+
import { CODEX_FAMILY_PROFILES } from "../../scripts/lib/model-family-resolver.mjs";
|
|
30
28
|
|
|
31
29
|
export const STATES = Object.freeze({
|
|
32
30
|
PLANNING: "PLANNING",
|
|
@@ -47,13 +45,12 @@ const ESCALATION_CHAIN_CONFIG_PATH = ".triflux/config/escalation-chain.json";
|
|
|
47
45
|
const DEFAULT_CLAUDE_MODEL = "fable";
|
|
48
46
|
|
|
49
47
|
// Escalation chain (2026-09-22 정책):
|
|
50
|
-
// 1. codex
|
|
48
|
+
// 1. codex astra_max: 최난도 단일 작업용 Codex escalation 단계
|
|
51
49
|
// 2. claude fable alias: Claude CLI가 최신 Fable tier로 해석하는 최종 수단
|
|
52
50
|
const DEFAULT_ESCALATION_CHAIN = Object.freeze([
|
|
53
51
|
Object.freeze({
|
|
54
52
|
cli: "codex",
|
|
55
|
-
|
|
56
|
-
profile: "gpt6_astra_max",
|
|
53
|
+
profile: "astra_max",
|
|
57
54
|
}),
|
|
58
55
|
Object.freeze({ cli: "claude", model: DEFAULT_CLAUDE_MODEL }),
|
|
59
56
|
]);
|
|
@@ -62,8 +59,8 @@ const STUCK_THRESHOLD = 3;
|
|
|
62
59
|
|
|
63
60
|
function normalizeEscalationProfile(profile) {
|
|
64
61
|
const normalized = normalizeCodexProfileName(profile);
|
|
65
|
-
return normalized === "ultra" || normalized === "
|
|
66
|
-
? "
|
|
62
|
+
return normalized === "ultra" || normalized === "astra_ultra"
|
|
63
|
+
? "astra_max"
|
|
67
64
|
: normalized;
|
|
68
65
|
}
|
|
69
66
|
|
|
@@ -307,26 +304,21 @@ function normalizeEscalationChainEntry(entry, index, source) {
|
|
|
307
304
|
`${source}: chain[${index}].cli must be a non-empty string`,
|
|
308
305
|
);
|
|
309
306
|
}
|
|
307
|
+
const profile = typeof entry.profile === "string" ? entry.profile.trim() : "";
|
|
308
|
+
const normalizedProfile = normalizeEscalationProfile(profile);
|
|
309
|
+
if (
|
|
310
|
+
entry.cli === "codex" &&
|
|
311
|
+
Object.hasOwn(CODEX_FAMILY_PROFILES, normalizedProfile)
|
|
312
|
+
) {
|
|
313
|
+
return { cli: entry.cli, profile: normalizedProfile };
|
|
314
|
+
}
|
|
310
315
|
if (typeof entry.model !== "string" || entry.model.trim() === "") {
|
|
311
316
|
throw new Error(
|
|
312
317
|
`${source}: chain[${index}].model must be a non-empty string`,
|
|
313
318
|
);
|
|
314
319
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
cli: entry.cli,
|
|
318
|
-
model: entry.model,
|
|
319
|
-
};
|
|
320
|
-
if (typeof entry.profile === "string" && entry.profile.trim() !== "") {
|
|
321
|
-
const profile = entry.profile.trim();
|
|
322
|
-
normalized.profile =
|
|
323
|
-
entry.cli === "codex" ? normalizeEscalationProfile(profile) : profile;
|
|
324
|
-
// A remapped legacy profile must not keep reporting its old model.
|
|
325
|
-
if (normalized.profile !== profile) {
|
|
326
|
-
normalized.model =
|
|
327
|
-
canonicalCodexProfileModel(normalized.profile) ?? normalized.model;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
320
|
+
const normalized = { cli: entry.cli, model: entry.model };
|
|
321
|
+
if (profile) normalized.profile = profile;
|
|
330
322
|
return normalized;
|
|
331
323
|
}
|
|
332
324
|
|
|
@@ -4,12 +4,13 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { StringDecoder } from "node:string_decoder";
|
|
6
6
|
|
|
7
|
-
// https://platform.claude.com/docs/en/build-with-claude/context-windows 2026-10-
|
|
7
|
+
// https://platform.claude.com/docs/en/build-with-claude/context-windows 2026-10-12
|
|
8
8
|
// https://developers.openai.com/api/docs/models 2026-10-08
|
|
9
9
|
const MODEL_CONTEXT = [
|
|
10
10
|
{
|
|
11
11
|
cli: "claude",
|
|
12
|
-
model:
|
|
12
|
+
model:
|
|
13
|
+
/^(?:claude-)?(?:fable|opus|sonnet|haiku)-(?:[5-9]|[1-9]\d+)(?:[.-]\d+)*(?:$|[-[])/i,
|
|
13
14
|
tokens: 1_000_000,
|
|
14
15
|
source:
|
|
15
16
|
"https://platform.claude.com/docs/en/build-with-claude/context-windows",
|
package/package.json
CHANGED
|
@@ -10,127 +10,127 @@ import { resolveGeminiProfileForPurpose } from "./gemini-profiles.mjs";
|
|
|
10
10
|
|
|
11
11
|
const policy = {
|
|
12
12
|
executor: {
|
|
13
|
-
profile: "
|
|
13
|
+
profile: "sol_high",
|
|
14
14
|
timeoutSec: 1080,
|
|
15
15
|
mcpHint: "implement",
|
|
16
16
|
subcommand: "exec",
|
|
17
17
|
},
|
|
18
18
|
codex: {
|
|
19
|
-
profile: "
|
|
19
|
+
profile: "sol_high",
|
|
20
20
|
timeoutSec: 1080,
|
|
21
21
|
mcpHint: "implement",
|
|
22
22
|
subcommand: "exec",
|
|
23
23
|
},
|
|
24
24
|
"build-fixer": {
|
|
25
|
-
profile: "
|
|
25
|
+
profile: "luna_high",
|
|
26
26
|
timeoutSec: 540,
|
|
27
27
|
mcpHint: "implement",
|
|
28
28
|
subcommand: "exec",
|
|
29
29
|
},
|
|
30
30
|
cleanup: {
|
|
31
|
-
profile: "
|
|
31
|
+
profile: "sol_med",
|
|
32
32
|
timeoutSec: 540,
|
|
33
33
|
mcpHint: "implement",
|
|
34
34
|
subcommand: "exec",
|
|
35
35
|
},
|
|
36
36
|
deslop: {
|
|
37
|
-
profile: "
|
|
37
|
+
profile: "sol_med",
|
|
38
38
|
timeoutSec: 540,
|
|
39
39
|
mcpHint: "implement",
|
|
40
40
|
subcommand: "exec",
|
|
41
41
|
},
|
|
42
42
|
debugger: {
|
|
43
|
-
profile: "
|
|
43
|
+
profile: "astra_xhigh",
|
|
44
44
|
timeoutSec: 900,
|
|
45
45
|
mcpHint: "implement",
|
|
46
46
|
subcommand: "exec",
|
|
47
47
|
},
|
|
48
48
|
"deep-executor": {
|
|
49
|
-
profile: "
|
|
49
|
+
profile: "astra_xhigh",
|
|
50
50
|
timeoutSec: 3600,
|
|
51
51
|
mcpHint: "implement",
|
|
52
52
|
subcommand: "exec",
|
|
53
53
|
},
|
|
54
54
|
architect: {
|
|
55
|
-
profile: "
|
|
55
|
+
profile: "astra_xhigh",
|
|
56
56
|
timeoutSec: 3600,
|
|
57
57
|
mcpHint: "analyze",
|
|
58
58
|
subcommand: "exec",
|
|
59
59
|
},
|
|
60
60
|
critic: {
|
|
61
|
-
profile: "
|
|
61
|
+
profile: "astra_xhigh",
|
|
62
62
|
timeoutSec: 3600,
|
|
63
63
|
mcpHint: "analyze",
|
|
64
64
|
subcommand: "exec",
|
|
65
65
|
},
|
|
66
66
|
planner: {
|
|
67
|
-
profile: "
|
|
67
|
+
profile: "astra_xhigh",
|
|
68
68
|
timeoutSec: 3600,
|
|
69
69
|
mcpHint: "analyze",
|
|
70
70
|
subcommand: "exec",
|
|
71
71
|
},
|
|
72
72
|
analyst: {
|
|
73
|
-
profile: "
|
|
73
|
+
profile: "astra_xhigh",
|
|
74
74
|
timeoutSec: 3600,
|
|
75
75
|
mcpHint: "analyze",
|
|
76
76
|
subcommand: "exec",
|
|
77
77
|
},
|
|
78
78
|
"code-reviewer": {
|
|
79
|
-
profile: "
|
|
79
|
+
profile: "sol_high",
|
|
80
80
|
timeoutSec: 1800,
|
|
81
81
|
mcpHint: "review",
|
|
82
82
|
subcommand: "review",
|
|
83
83
|
},
|
|
84
84
|
"quality-reviewer": {
|
|
85
|
-
profile: "
|
|
85
|
+
profile: "sol_high",
|
|
86
86
|
timeoutSec: 1800,
|
|
87
87
|
mcpHint: "review",
|
|
88
88
|
subcommand: "review",
|
|
89
89
|
},
|
|
90
90
|
"security-reviewer": {
|
|
91
|
-
profile: "
|
|
91
|
+
profile: "astra_xhigh",
|
|
92
92
|
timeoutSec: 1800,
|
|
93
93
|
mcpHint: "review",
|
|
94
94
|
subcommand: "review",
|
|
95
95
|
},
|
|
96
96
|
scientist: {
|
|
97
|
-
profile: "
|
|
97
|
+
profile: "sol_high",
|
|
98
98
|
timeoutSec: 1440,
|
|
99
99
|
mcpHint: "analyze",
|
|
100
100
|
subcommand: "exec",
|
|
101
101
|
},
|
|
102
102
|
"document-specialist": {
|
|
103
|
-
profile: "
|
|
103
|
+
profile: "sol_high",
|
|
104
104
|
timeoutSec: 1440,
|
|
105
105
|
mcpHint: "analyze",
|
|
106
106
|
subcommand: "exec",
|
|
107
107
|
},
|
|
108
108
|
"scientist-deep": {
|
|
109
|
-
profile: "
|
|
109
|
+
profile: "astra_xhigh",
|
|
110
110
|
timeoutSec: 3600,
|
|
111
111
|
mcpHint: "analyze",
|
|
112
112
|
subcommand: "exec",
|
|
113
113
|
},
|
|
114
114
|
verifier: {
|
|
115
|
-
profile: "
|
|
115
|
+
profile: "sol_high",
|
|
116
116
|
timeoutSec: 1200,
|
|
117
117
|
mcpHint: "review",
|
|
118
118
|
subcommand: "review",
|
|
119
119
|
},
|
|
120
120
|
"test-engineer": {
|
|
121
|
-
profile: "
|
|
121
|
+
profile: "sol_high",
|
|
122
122
|
timeoutSec: 1200,
|
|
123
123
|
mcpHint: "implement",
|
|
124
124
|
subcommand: "exec",
|
|
125
125
|
},
|
|
126
126
|
"qa-tester": {
|
|
127
|
-
profile: "
|
|
127
|
+
profile: "sol_high",
|
|
128
128
|
timeoutSec: 1200,
|
|
129
129
|
mcpHint: "review",
|
|
130
130
|
subcommand: "review",
|
|
131
131
|
},
|
|
132
132
|
spark: {
|
|
133
|
-
profile: "
|
|
133
|
+
profile: "luna_low",
|
|
134
134
|
timeoutSec: 180,
|
|
135
135
|
mcpHint: "implement",
|
|
136
136
|
subcommand: "exec",
|
|
@@ -138,13 +138,13 @@ const policy = {
|
|
|
138
138
|
// These entries are only for explicit direct-Codex overrides. agent-map.json
|
|
139
139
|
// remains the provider-selection authority for their normal execution.
|
|
140
140
|
designer: {
|
|
141
|
-
profile: "
|
|
141
|
+
profile: "astra_xhigh",
|
|
142
142
|
timeoutSec: 900,
|
|
143
143
|
mcpHint: "implement",
|
|
144
144
|
subcommand: "exec",
|
|
145
145
|
},
|
|
146
146
|
writer: {
|
|
147
|
-
profile: "
|
|
147
|
+
profile: "luna_high",
|
|
148
148
|
timeoutSec: 900,
|
|
149
149
|
mcpHint: "implement",
|
|
150
150
|
subcommand: "exec",
|
|
@@ -167,13 +167,13 @@ const DEFAULT_CODEX_AGENT = "executor";
|
|
|
167
167
|
|
|
168
168
|
const CANONICAL_CODEX_PROFILE_OVERRIDES = Object.freeze(
|
|
169
169
|
new Set([
|
|
170
|
-
"
|
|
171
|
-
"
|
|
172
|
-
"
|
|
173
|
-
"
|
|
174
|
-
"
|
|
175
|
-
"
|
|
176
|
-
"
|
|
170
|
+
"luna_low",
|
|
171
|
+
"luna_high",
|
|
172
|
+
"sol_med",
|
|
173
|
+
"sol_high",
|
|
174
|
+
"astra_xhigh",
|
|
175
|
+
"astra_max",
|
|
176
|
+
"astra_ultra",
|
|
177
177
|
]),
|
|
178
178
|
);
|
|
179
179
|
const ULTRA_ELIGIBLE_CODEX_AGENTS = Object.freeze(
|
|
@@ -183,8 +183,8 @@ const ULTRA_ELIGIBLE_CODEX_AGENTS = Object.freeze(
|
|
|
183
183
|
const NESTED_GLOBAL_PROFILE_OVERRIDES = new Set([
|
|
184
184
|
"max",
|
|
185
185
|
"ultra",
|
|
186
|
-
"
|
|
187
|
-
"
|
|
186
|
+
"astra_max",
|
|
187
|
+
"astra_ultra",
|
|
188
188
|
]);
|
|
189
189
|
|
|
190
190
|
export function resolveCodexAgentPolicy(agent) {
|
|
@@ -219,9 +219,9 @@ export function resolveCodexAgentProfile(
|
|
|
219
219
|
|
|
220
220
|
const canonical =
|
|
221
221
|
requested === "max"
|
|
222
|
-
? "
|
|
222
|
+
? "astra_max"
|
|
223
223
|
: requested === "ultra"
|
|
224
|
-
? "
|
|
224
|
+
? "astra_ultra"
|
|
225
225
|
: requested;
|
|
226
226
|
if (!CANONICAL_CODEX_PROFILE_OVERRIDES.has(canonical)) {
|
|
227
227
|
if (allowCustom) return requested;
|
|
@@ -229,9 +229,9 @@ export function resolveCodexAgentProfile(
|
|
|
229
229
|
`[agent-route-policy] unsupported profile override: ${requested}`,
|
|
230
230
|
);
|
|
231
231
|
}
|
|
232
|
-
if (canonical !== "
|
|
232
|
+
if (canonical !== "astra_ultra") return canonical;
|
|
233
233
|
return nested || !ULTRA_ELIGIBLE_CODEX_AGENTS.has(agent)
|
|
234
|
-
? "
|
|
234
|
+
? "astra_max"
|
|
235
235
|
: canonical;
|
|
236
236
|
}
|
|
237
237
|
|
|
@@ -270,7 +270,7 @@ export function resolveNestedCodexAgentProfile(
|
|
|
270
270
|
});
|
|
271
271
|
const { effort } = resolveCodexProfileConfigValues(candidate, { codexHome });
|
|
272
272
|
if (!effort) return fallback;
|
|
273
|
-
return effort.toLowerCase() === "ultra" ? "
|
|
273
|
+
return effort.toLowerCase() === "ultra" ? "astra_max" : candidate;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
const EXPECTED_DURATION_BY_AGENT = Object.freeze({
|
|
@@ -408,13 +408,13 @@ async function runCli() {
|
|
|
408
408
|
const module = await import(pathToFileURL(geminiProfilesModule).href);
|
|
409
409
|
resolver = (agent) => {
|
|
410
410
|
try {
|
|
411
|
-
return module.resolveGeminiProfileForPurpose(agent) || "
|
|
411
|
+
return module.resolveGeminiProfileForPurpose(agent) || "flash";
|
|
412
412
|
} catch {
|
|
413
|
-
return "
|
|
413
|
+
return "flash";
|
|
414
414
|
}
|
|
415
415
|
};
|
|
416
416
|
} catch {
|
|
417
|
-
resolver = () => "
|
|
417
|
+
resolver = () => "flash";
|
|
418
418
|
}
|
|
419
419
|
}
|
|
420
420
|
process.stdout.write(
|
|
@@ -9,6 +9,12 @@ import {
|
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { dirname, join } from "node:path";
|
|
11
11
|
import { writeRotatedBackup } from "./backup-rotation.mjs";
|
|
12
|
+
import {
|
|
13
|
+
CODEX_FAMILY_PROFILES,
|
|
14
|
+
modelFamilyPaths,
|
|
15
|
+
resolveModelFamilies,
|
|
16
|
+
writeModelFile,
|
|
17
|
+
} from "./model-family-resolver.mjs";
|
|
12
18
|
|
|
13
19
|
const EFFORT_BY_SUFFIX = {
|
|
14
20
|
ultra: "ultra",
|
|
@@ -20,41 +26,38 @@ const EFFORT_BY_SUFFIX = {
|
|
|
20
26
|
low: "low",
|
|
21
27
|
};
|
|
22
28
|
|
|
23
|
-
const CANONICAL_PROFILE_VALUES = Object.freeze({
|
|
24
|
-
gpt6_luna_low: { model: "gpt-6-luna", effort: "low" },
|
|
25
|
-
gpt6_luna_high: { model: "gpt-6-luna", effort: "high" },
|
|
26
|
-
gpt61_sol_med: { model: "gpt-6.1-sol", effort: "medium" },
|
|
27
|
-
gpt61_sol_high: { model: "gpt-6.1-sol", effort: "high" },
|
|
28
|
-
gpt6_astra_xhigh: { model: "gpt-6-astra", effort: "xhigh" },
|
|
29
|
-
gpt6_astra_max: { model: "gpt-6-astra", effort: "max" },
|
|
30
|
-
gpt6_astra_ultra: { model: "gpt-6-astra", effort: "ultra" },
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// GPT-5.6 lane names map to the GPT-6 lane with the same effort. GPT-6 has no
|
|
34
|
-
// Terra tier, so Terra lanes move to Sol. GPT-6 Sol lane names map to GPT-6.1
|
|
35
|
-
// Sol with the same effort.
|
|
36
29
|
const LEGACY_CANONICAL_PROFILE_ALIASES = Object.freeze({
|
|
37
|
-
gpt56_sol_xhigh: "
|
|
38
|
-
gpt56_sol_max: "
|
|
39
|
-
gpt56_sol_ultra: "
|
|
40
|
-
gpt56_terra_high: "
|
|
41
|
-
gpt56_terra_med: "
|
|
42
|
-
gpt56_luna_low: "gpt6_luna_low",
|
|
43
|
-
gpt6_sol_high: "gpt61_sol_high",
|
|
44
|
-
gpt6_sol_med: "gpt61_sol_med",
|
|
30
|
+
gpt56_sol_xhigh: "astra_xhigh",
|
|
31
|
+
gpt56_sol_max: "astra_max",
|
|
32
|
+
gpt56_sol_ultra: "astra_ultra",
|
|
33
|
+
gpt56_terra_high: "sol_high",
|
|
34
|
+
gpt56_terra_med: "sol_med",
|
|
45
35
|
});
|
|
46
36
|
|
|
47
37
|
export function normalizeCodexProfileName(profileName) {
|
|
48
38
|
const normalized = String(profileName ?? "").trim();
|
|
49
|
-
|
|
39
|
+
const legacy = LEGACY_CANONICAL_PROFILE_ALIASES[normalized];
|
|
40
|
+
if (legacy) return legacy;
|
|
41
|
+
const familyName = normalized.replace(/^gpt\d+_(sol|astra|luna)_/, "$1_");
|
|
42
|
+
return Object.hasOwn(CODEX_FAMILY_PROFILES, familyName)
|
|
43
|
+
? familyName
|
|
44
|
+
: normalized;
|
|
50
45
|
}
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
export function ensureManagedCodexProfiles(opts = {}) {
|
|
48
|
+
const { codexHome } = modelFamilyPaths(opts);
|
|
49
|
+
const { profiles } = resolveModelFamilies("codex", opts);
|
|
50
|
+
let changed = 0;
|
|
51
|
+
for (const [name, { model, effort }] of Object.entries(profiles)) {
|
|
52
|
+
if (
|
|
53
|
+
writeModelFile(
|
|
54
|
+
join(codexHome, `${name}.config.toml`),
|
|
55
|
+
`model = "${model}"\nmodel_reasoning_effort = "${effort}"\n`,
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
changed++;
|
|
59
|
+
}
|
|
60
|
+
return { changed, profiles };
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
function readProfileScalar(raw, key) {
|
|
@@ -70,7 +73,7 @@ function readProfileScalar(raw, key) {
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
/**
|
|
73
|
-
* Resolve a Codex effort profile name (e.g. "
|
|
76
|
+
* Resolve a Codex effort profile name (e.g. "sol_high") to the `-c key=value`
|
|
74
77
|
* config overrides that `codex exec` accepts, so the headless CLI lanes can
|
|
75
78
|
* select a profile WITHOUT `codex exec --profile <name>`. codex 0.134+ rejects
|
|
76
79
|
* `--profile X` whenever config.toml still contains an inline `[profiles.X]`
|
|
@@ -118,12 +121,20 @@ export function codexProfileConfigOverrides(profileName, opts = {}) {
|
|
|
118
121
|
opts,
|
|
119
122
|
);
|
|
120
123
|
if (opts.enforceCanonicalProfile === true) {
|
|
121
|
-
|
|
122
|
-
|
|
124
|
+
if (Object.hasOwn(CODEX_FAMILY_PROFILES, normalizedProfileName)) {
|
|
125
|
+
({ model, effort } = resolveModelFamilies("codex", {
|
|
126
|
+
...opts,
|
|
127
|
+
allowCommand: false,
|
|
128
|
+
persist: false,
|
|
129
|
+
}).profiles[normalizedProfileName]);
|
|
130
|
+
}
|
|
123
131
|
}
|
|
124
132
|
if (opts.disallowUltra === true && effort?.toLowerCase() === "ultra") {
|
|
125
|
-
model = "
|
|
126
|
-
|
|
133
|
+
({ model, effort } = resolveModelFamilies("codex", {
|
|
134
|
+
...opts,
|
|
135
|
+
allowCommand: false,
|
|
136
|
+
persist: false,
|
|
137
|
+
}).profiles.astra_max);
|
|
127
138
|
}
|
|
128
139
|
const overrides = [];
|
|
129
140
|
if (model) overrides.push(`model="${model}"`);
|
|
@@ -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
|
+
}
|