@triflux/core 10.86.1 → 10.87.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/bridge.mjs CHANGED
@@ -665,7 +665,16 @@ function buildRetryCliInvocation(cli) {
665
665
  argv: [],
666
666
  };
667
667
  if (cli.profile) invocation.profile = cli.profile;
668
- if (cli.cli === "codex" && cli.profile) {
668
+ if (cli.family) invocation.family = cli.family;
669
+ if (cli.effort) invocation.effort = cli.effort;
670
+ if (cli.cli === "codex" && cli.family && cli.effort) {
671
+ invocation.argv.push(
672
+ "--profile",
673
+ cli.family,
674
+ "-c",
675
+ `model_reasoning_effort="${cli.effort}"`,
676
+ );
677
+ } else if (cli.cli === "codex" && cli.profile) {
669
678
  invocation.argv.push("--profile", cli.profile);
670
679
  }
671
680
  return invocation;
@@ -24,7 +24,13 @@ import {
24
24
  import { dirname, join } from "node:path";
25
25
 
26
26
  import { normalizeCodexProfileName } from "../../scripts/lib/codex-profile-config.mjs";
27
- import { CODEX_FAMILY_PROFILES } from "../../scripts/lib/model-family-resolver.mjs";
27
+ import {
28
+ EFFORT_ORDER,
29
+ modelEffortFromProfile,
30
+ profileFromModelEffort,
31
+ resolveCodexModelEffort,
32
+ } from "../../scripts/lib/model-effort.mjs";
33
+ import { AGY_FAMILY_PROFILES } from "../../scripts/lib/model-family-resolver.mjs";
28
34
 
29
35
  export const STATES = Object.freeze({
30
36
  PLANNING: "PLANNING",
@@ -50,7 +56,8 @@ const DEFAULT_CLAUDE_MODEL = "fable";
50
56
  const DEFAULT_ESCALATION_CHAIN = Object.freeze([
51
57
  Object.freeze({
52
58
  cli: "codex",
53
- profile: "astra_max",
59
+ family: "astra",
60
+ effort: "max",
54
61
  }),
55
62
  Object.freeze({ cli: "claude", model: DEFAULT_CLAUDE_MODEL }),
56
63
  ]);
@@ -155,6 +162,39 @@ export function createRetryStateMachine(options = {}) {
155
162
  chain: state.cliChain,
156
163
  });
157
164
  }
165
+ const current = state.cliChain[state.cliIndex];
166
+ const next = state.cliChain[state.cliIndex + 1];
167
+ if (
168
+ current.cli === "codex" &&
169
+ next.cli === "codex" &&
170
+ current.family &&
171
+ next.family
172
+ ) {
173
+ try {
174
+ const base = resolveCodexModelEffort(current, options);
175
+ const promoted = resolveCodexModelEffort(next, options);
176
+ if (
177
+ promoted.effort !== next.effort &&
178
+ base.model === promoted.model &&
179
+ base.effort === promoted.effort
180
+ ) {
181
+ return transition(STATES.BUDGET_EXCEEDED, {
182
+ reason: "escalation-not-promoted",
183
+ escalated: false,
184
+ model: promoted.model,
185
+ requestedEffort: next.effort,
186
+ effort: promoted.effort,
187
+ });
188
+ }
189
+ } catch (error) {
190
+ if (error.code !== "unsupported-effort") throw error;
191
+ return transition(STATES.BUDGET_EXCEEDED, {
192
+ reason: "unsupported-escalation-effort",
193
+ escalated: false,
194
+ error: error.message,
195
+ });
196
+ }
197
+ }
158
198
  state.cliIndex += 1;
159
199
  state.iterations = 0;
160
200
  state.stuckCounter = 0;
@@ -306,11 +346,34 @@ function normalizeEscalationChainEntry(entry, index, source) {
306
346
  }
307
347
  const profile = typeof entry.profile === "string" ? entry.profile.trim() : "";
308
348
  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 };
349
+ const provider = entry.cli === "agy" ? "antigravity" : entry.cli;
350
+ if (provider === "codex" || provider === "antigravity") {
351
+ const explicit = entry.family !== undefined || entry.effort !== undefined;
352
+ let spec = explicit
353
+ ? typeof entry.family === "string" && typeof entry.effort === "string"
354
+ ? modelEffortFromProfile(provider, entry.family, entry.effort)
355
+ : null
356
+ : modelEffortFromProfile(
357
+ provider,
358
+ provider === "codex" ? normalizedProfile : profile,
359
+ );
360
+ if (explicit && !spec)
361
+ throw new Error(`${source}: chain[${index}] has invalid family/effort`);
362
+ if (spec) {
363
+ if (provider === "codex" && spec.effort === "ultra")
364
+ spec = { family: "astra", effort: "max" };
365
+ const legacy = profileFromModelEffort(provider, spec);
366
+ const normalized = { cli: entry.cli, ...spec };
367
+ if (legacy) normalized.profile = legacy;
368
+ if (provider === "antigravity" && legacy) {
369
+ normalized.model = entry.model || AGY_FAMILY_PROFILES[legacy]?.model;
370
+ if (typeof normalized.model !== "string" || !normalized.model.trim())
371
+ throw new Error(
372
+ `${source}: chain[${index}].model must be a non-empty string`,
373
+ );
374
+ }
375
+ return normalized;
376
+ }
314
377
  }
315
378
  if (typeof entry.model !== "string" || entry.model.trim() === "") {
316
379
  throw new Error(
@@ -319,6 +382,11 @@ function normalizeEscalationChainEntry(entry, index, source) {
319
382
  }
320
383
  const normalized = { cli: entry.cli, model: entry.model };
321
384
  if (profile) normalized.profile = profile;
385
+ if (entry.effort !== undefined) {
386
+ if (!EFFORT_ORDER.includes(entry.effort))
387
+ throw new Error(`${source}: chain[${index}] has invalid effort`);
388
+ normalized.effort = entry.effort;
389
+ }
322
390
  return normalized;
323
391
  }
324
392
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/core",
3
- "version": "10.86.1",
3
+ "version": "10.87.0",
4
4
  "description": "triflux core — CLI routing, adapters. Zero native dependencies.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",
@@ -9,130 +9,134 @@ import {
9
9
  resolveCodexProfileConfigValues,
10
10
  } from "./codex-profile-config.mjs";
11
11
  import { resolveGeminiProfileForPurpose } from "./gemini-profiles.mjs";
12
+ import {
13
+ modelEffortFromProfile,
14
+ profileFromModelEffort,
15
+ } from "./model-effort.mjs";
12
16
 
13
17
  const policy = {
14
18
  executor: {
15
- profile: "sol_high",
19
+ codex: { family: "sol", effort: "high" },
16
20
  timeoutSec: 1080,
17
21
  mcpHint: "implement",
18
22
  subcommand: "exec",
19
23
  },
20
24
  codex: {
21
- profile: "sol_high",
25
+ codex: { family: "sol", effort: "high" },
22
26
  timeoutSec: 1080,
23
27
  mcpHint: "implement",
24
28
  subcommand: "exec",
25
29
  },
26
30
  "build-fixer": {
27
- profile: "luna_high",
31
+ codex: { family: "luna", effort: "high" },
28
32
  timeoutSec: 540,
29
33
  mcpHint: "implement",
30
34
  subcommand: "exec",
31
35
  },
32
36
  cleanup: {
33
- profile: "sol_med",
37
+ codex: { family: "sol", effort: "medium" },
34
38
  timeoutSec: 540,
35
39
  mcpHint: "implement",
36
40
  subcommand: "exec",
37
41
  },
38
42
  deslop: {
39
- profile: "sol_med",
43
+ codex: { family: "sol", effort: "medium" },
40
44
  timeoutSec: 540,
41
45
  mcpHint: "implement",
42
46
  subcommand: "exec",
43
47
  },
44
48
  debugger: {
45
- profile: "astra_xhigh",
49
+ codex: { family: "astra", effort: "xhigh" },
46
50
  timeoutSec: 900,
47
51
  mcpHint: "implement",
48
52
  subcommand: "exec",
49
53
  },
50
54
  "deep-executor": {
51
- profile: "astra_xhigh",
55
+ codex: { family: "astra", effort: "xhigh" },
52
56
  timeoutSec: 3600,
53
57
  mcpHint: "implement",
54
58
  subcommand: "exec",
55
59
  },
56
60
  architect: {
57
- profile: "astra_xhigh",
61
+ codex: { family: "astra", effort: "xhigh" },
58
62
  timeoutSec: 3600,
59
63
  mcpHint: "analyze",
60
64
  subcommand: "exec",
61
65
  },
62
66
  critic: {
63
- profile: "astra_xhigh",
67
+ codex: { family: "astra", effort: "xhigh" },
64
68
  timeoutSec: 3600,
65
69
  mcpHint: "analyze",
66
70
  subcommand: "exec",
67
71
  },
68
72
  planner: {
69
- profile: "astra_xhigh",
73
+ codex: { family: "astra", effort: "xhigh" },
70
74
  timeoutSec: 3600,
71
75
  mcpHint: "analyze",
72
76
  subcommand: "exec",
73
77
  },
74
78
  analyst: {
75
- profile: "astra_xhigh",
79
+ codex: { family: "astra", effort: "xhigh" },
76
80
  timeoutSec: 3600,
77
81
  mcpHint: "analyze",
78
82
  subcommand: "exec",
79
83
  },
80
84
  "code-reviewer": {
81
- profile: "sol_high",
85
+ codex: { family: "sol", effort: "high" },
82
86
  timeoutSec: 1800,
83
87
  mcpHint: "review",
84
88
  subcommand: "review",
85
89
  },
86
90
  "quality-reviewer": {
87
- profile: "sol_high",
91
+ codex: { family: "sol", effort: "high" },
88
92
  timeoutSec: 1800,
89
93
  mcpHint: "review",
90
94
  subcommand: "review",
91
95
  },
92
96
  "security-reviewer": {
93
- profile: "astra_xhigh",
97
+ codex: { family: "astra", effort: "xhigh" },
94
98
  timeoutSec: 1800,
95
99
  mcpHint: "review",
96
100
  subcommand: "review",
97
101
  },
98
102
  scientist: {
99
- profile: "sol_high",
103
+ codex: { family: "sol", effort: "high" },
100
104
  timeoutSec: 1440,
101
105
  mcpHint: "analyze",
102
106
  subcommand: "exec",
103
107
  },
104
108
  "document-specialist": {
105
- profile: "terra_high",
109
+ codex: { family: "terra", effort: "high" },
106
110
  timeoutSec: 1440,
107
111
  mcpHint: "analyze",
108
112
  subcommand: "exec",
109
113
  },
110
114
  "scientist-deep": {
111
- profile: "astra_xhigh",
115
+ codex: { family: "astra", effort: "xhigh" },
112
116
  timeoutSec: 3600,
113
117
  mcpHint: "analyze",
114
118
  subcommand: "exec",
115
119
  },
116
120
  verifier: {
117
- profile: "sol_high",
121
+ codex: { family: "sol", effort: "high" },
118
122
  timeoutSec: 1200,
119
123
  mcpHint: "review",
120
124
  subcommand: "review",
121
125
  },
122
126
  "test-engineer": {
123
- profile: "sol_high",
127
+ codex: { family: "sol", effort: "high" },
124
128
  timeoutSec: 1200,
125
129
  mcpHint: "implement",
126
130
  subcommand: "exec",
127
131
  },
128
132
  "qa-tester": {
129
- profile: "sol_high",
133
+ codex: { family: "sol", effort: "high" },
130
134
  timeoutSec: 1200,
131
135
  mcpHint: "review",
132
136
  subcommand: "review",
133
137
  },
134
138
  spark: {
135
- profile: "luna_low",
139
+ codex: { family: "luna", effort: "low" },
136
140
  timeoutSec: 180,
137
141
  mcpHint: "implement",
138
142
  subcommand: "exec",
@@ -140,13 +144,13 @@ const policy = {
140
144
  // These entries are only for explicit direct-Codex overrides. agent-map.json
141
145
  // remains the provider-selection authority for their normal execution.
142
146
  designer: {
143
- profile: "astra_xhigh",
147
+ codex: { family: "astra", effort: "xhigh" },
144
148
  timeoutSec: 900,
145
149
  mcpHint: "implement",
146
150
  subcommand: "exec",
147
151
  },
148
152
  writer: {
149
- profile: "terra_high",
153
+ codex: { family: "terra", effort: "high" },
150
154
  timeoutSec: 900,
151
155
  mcpHint: "implement",
152
156
  subcommand: "exec",
@@ -154,7 +158,11 @@ const policy = {
154
158
  };
155
159
 
156
160
  function freezePolicy(entry) {
157
- return Object.freeze({ ...entry });
161
+ return Object.freeze({
162
+ ...entry,
163
+ codex: Object.freeze({ ...entry.codex }),
164
+ profile: profileFromModelEffort("codex", entry.codex),
165
+ });
158
166
  }
159
167
 
160
168
  const CODEX_AGENT_POLICY = Object.freeze(
@@ -264,16 +272,16 @@ export function resolveNestedCodexAgentProfile(
264
272
  : NESTED_GLOBAL_PROFILE_OVERRIDES.has(globalOverride)
265
273
  ? globalOverride
266
274
  : "auto";
267
- const fallback = resolveCodexAgentProfile(agent);
268
275
  const candidate = resolveCodexAgentProfile(agent, {
269
276
  profileOverride: effectiveOverride,
270
277
  retryProfile,
271
278
  nested: true,
272
279
  allowCustom: true,
273
280
  });
274
- const { effort } = resolveCodexProfileConfigValues(candidate, { codexHome });
275
- if (!effort) return fallback;
276
- return effort.toLowerCase() === "ultra" ? "astra_max" : candidate;
281
+ const { effort } =
282
+ modelEffortFromProfile("codex", candidate) ||
283
+ resolveCodexProfileConfigValues(candidate, { codexHome });
284
+ return effort?.toLowerCase() === "ultra" ? "astra_max" : candidate;
277
285
  }
278
286
 
279
287
  const EXPECTED_DURATION_BY_AGENT = Object.freeze({
@@ -365,6 +373,7 @@ export function resolveAgentRoute(
365
373
  );
366
374
  }
367
375
  }
376
+ const { family, effort } = modelEffortFromProfile("codex", resolvedOverride);
368
377
  return [
369
378
  provider,
370
379
  policy.profile,
@@ -378,6 +387,8 @@ export function resolveAgentRoute(
378
387
  NATIVE_CODEX_AGENTS.has(agent),
379
388
  byAuthor ? reviewAuthor : "",
380
389
  usageSwitch,
390
+ effort,
391
+ family,
381
392
  ];
382
393
  }
383
394
 
@@ -9,6 +9,11 @@ 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
+ modelEffortFromProfile,
14
+ normalizeCodexProfileName,
15
+ resolveCodexModelEffort,
16
+ } from "./model-effort.mjs";
12
17
  import {
13
18
  CODEX_FAMILY_PROFILES,
14
19
  modelFamilyPaths,
@@ -16,46 +21,42 @@ import {
16
21
  writeModelFile,
17
22
  } from "./model-family-resolver.mjs";
18
23
 
19
- const EFFORT_BY_SUFFIX = {
20
- ultra: "ultra",
21
- max: "max",
22
- xhigh: "xhigh",
23
- high: "high",
24
- med: "medium",
25
- medium: "medium",
26
- low: "low",
27
- };
28
-
29
- const LEGACY_CANONICAL_PROFILE_ALIASES = Object.freeze({
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",
35
- });
24
+ export { normalizeCodexProfileName };
36
25
 
37
- export function normalizeCodexProfileName(profileName) {
38
- const normalized = String(profileName ?? "").trim();
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;
26
+ export function managedCodexProfileContents(opts = {}) {
27
+ const { profiles, families } = resolveModelFamilies("codex", opts);
28
+ const contents = {};
29
+ for (const [name, { model, effort }] of Object.entries(profiles)) {
30
+ contents[name] =
31
+ `model = "${model}"\nmodel_reasoning_effort = "${effort}"\n`;
32
+ }
33
+ for (const [family, { model }] of Object.entries(families)) {
34
+ contents[family] = `model = "${model}"\n`;
35
+ }
36
+ return { profiles, contents };
45
37
  }
46
38
 
47
39
  export function ensureManagedCodexProfiles(opts = {}) {
48
40
  const { codexHome } = modelFamilyPaths(opts);
49
- const { profiles } = resolveModelFamilies("codex", opts);
41
+ const { profiles, contents } = managedCodexProfileContents(opts);
50
42
  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++;
43
+ const families = new Set(
44
+ Object.values(CODEX_FAMILY_PROFILES).map(({ family }) => family),
45
+ );
46
+ for (const [name, content] of Object.entries(contents)) {
47
+ const path = join(codexHome, `${name}.config.toml`);
48
+ if (families.has(name) && existsSync(path)) {
49
+ const existing = readFileSync(path, "utf8");
50
+ if (existing !== content) {
51
+ const backup = writeRotatedBackup(path, existing, {
52
+ label: "bak-model-family",
53
+ suffix: stamp(),
54
+ mode: privateFileMode(path),
55
+ });
56
+ opts.notice?.(`Codex 가족 프로필 백업: ${backup}`);
57
+ }
58
+ }
59
+ if (writeModelFile(path, content)) changed++;
59
60
  }
60
61
  return { changed, profiles };
61
62
  }
@@ -72,45 +73,25 @@ function readProfileScalar(raw, key) {
72
73
  return value || null;
73
74
  }
74
75
 
75
- /**
76
- * Resolve a Codex effort profile name (e.g. "sol_high") to the `-c key=value`
77
- * config overrides that `codex exec` accepts, so the headless CLI lanes can
78
- * select a profile WITHOUT `codex exec --profile <name>`. codex 0.134+ rejects
79
- * `--profile X` whenever config.toml still contains an inline `[profiles.X]`
80
- * table (which codex itself re-injects when it rewrites config.toml), so the
81
- * `-c` form is immune and needs no config mutation / sanitize race.
82
- *
83
- * SSOT = the per-profile file `~/.codex/<name>.config.toml` written by
84
- * `tfx setup`; falls back to deriving the effort from the `<...>_<effort>`
85
- * naming convention when the file is absent. Returns the keys that resolve, in
86
- * `key="value"` TOML-scalar form ready to be shell-quoted by the caller.
87
- *
88
- * @param {string} profileName
89
- * @param {{ codexHome?: string }} [opts]
90
- * @returns {string[]} e.g. ['model="gpt-6.1-sol"', 'model_reasoning_effort="high"']
91
- */
76
+ // 관리 이름의 effort는 변환 표가 정한다. custom 파일은 ultra 제한 판정에 사용한다.
92
77
  export function resolveCodexProfileConfigValues(profileName, opts = {}) {
93
78
  if (typeof profileName !== "string" || !profileName) {
94
79
  return { model: null, effort: null };
95
80
  }
96
81
  const normalizedProfileName = normalizeCodexProfileName(profileName);
82
+ const managed = modelEffortFromProfile("codex", normalizedProfileName);
97
83
  const codexHome =
98
84
  opts.codexHome || process.env.CODEX_HOME || join(homedir(), ".codex");
99
85
  let model = null;
100
- let effort = null;
86
+ let effort = managed ? resolveCodexModelEffort(managed, opts).effort : null;
101
87
  try {
102
88
  const raw = readFileSync(
103
89
  join(codexHome, `${normalizedProfileName}.config.toml`),
104
90
  "utf8",
105
91
  );
106
92
  model = readProfileScalar(raw, "model");
107
- effort = readProfileScalar(raw, "model_reasoning_effort");
108
- } catch {
109
- const suffix = /_(ultra|max|xhigh|high|med|medium|low)$/.exec(
110
- normalizedProfileName,
111
- );
112
- if (suffix) effort = EFFORT_BY_SUFFIX[suffix[1]] || null;
113
- }
93
+ if (!managed) effort = readProfileScalar(raw, "model_reasoning_effort");
94
+ } catch {}
114
95
  return { model, effort };
115
96
  }
116
97
 
@@ -1,5 +1,6 @@
1
1
  import { copyFileSync, existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
+ import { normalizeGeminiProfileName } from "./model-effort.mjs";
3
4
  import {
4
5
  AGY_FAMILY_PROFILES,
5
6
  modelFamilyPaths,
@@ -17,11 +18,7 @@ const DEFAULT_GEMINI_PROFILES = {
17
18
  ),
18
19
  };
19
20
 
20
- export function normalizeGeminiProfileName(name) {
21
- const raw = String(name ?? "").trim();
22
- // Flash 가족은 뺐다. 옛 Flash 이름은 effort 와 상관없이 더 빠른 Argon Low 로 보낸다.
23
- return raw.replace(/^flash\d*(?:_(?:low|high))?$/, "argon_low");
24
- }
21
+ export { normalizeGeminiProfileName };
25
22
 
26
23
  // ── 용도별 effort (SSOT) ──
27
24
  // Argon 가족 안에서 역할에 따라 effort 를 나눈다. 키는 tfx 에이전트/역할 이름
@@ -0,0 +1,134 @@
1
+ import {
2
+ AGY_FAMILY_PROFILES,
3
+ CODEX_FAMILY_PROFILES,
4
+ readCodexModelCatalog,
5
+ resolveModelFamilies,
6
+ } from "./model-family-resolver.mjs";
7
+
8
+ export const EFFORT_ORDER = Object.freeze([
9
+ "none",
10
+ "low",
11
+ "medium",
12
+ "high",
13
+ "xhigh",
14
+ "max",
15
+ "ultra",
16
+ ]);
17
+
18
+ const LEGACY_CANONICAL_PROFILE_ALIASES = Object.freeze({
19
+ gpt56_sol_xhigh: "astra_xhigh",
20
+ gpt56_sol_max: "astra_max",
21
+ gpt56_sol_ultra: "astra_ultra",
22
+ gpt56_terra_high: "sol_high",
23
+ gpt56_terra_med: "sol_med",
24
+ });
25
+
26
+ export function normalizeCodexProfileName(profileName) {
27
+ const normalized = String(profileName ?? "").trim();
28
+ if (Object.hasOwn(LEGACY_CANONICAL_PROFILE_ALIASES, normalized))
29
+ return LEGACY_CANONICAL_PROFILE_ALIASES[normalized];
30
+ const familyName = normalized.replace(/^gpt\d+_(sol|astra|luna)_/, "$1_");
31
+ return Object.hasOwn(CODEX_FAMILY_PROFILES, familyName)
32
+ ? familyName
33
+ : normalized;
34
+ }
35
+
36
+ export function normalizeGeminiProfileName(name) {
37
+ const raw = String(name ?? "").trim();
38
+ // 옛 Flash 이름은 effort와 상관없이 Argon Low로 보낸다.
39
+ return raw.replace(/^flash\d*(?:_(?:low|high))?$/, "argon_low");
40
+ }
41
+
42
+ export function modelEffortFromProfile(cli, profile, effort) {
43
+ if (effort !== undefined) {
44
+ const definitions =
45
+ cli === "codex"
46
+ ? CODEX_FAMILY_PROFILES
47
+ : cli === "antigravity" || cli === "agy"
48
+ ? AGY_FAMILY_PROFILES
49
+ : {};
50
+ return EFFORT_ORDER.includes(effort) &&
51
+ Object.values(definitions).some(({ family }) => family === profile)
52
+ ? { family: profile, effort }
53
+ : null;
54
+ }
55
+ let definition;
56
+ if (cli === "codex") {
57
+ const normalized = normalizeCodexProfileName(profile);
58
+ const name =
59
+ normalized === "max" || normalized === "ultra"
60
+ ? `astra_${normalized}`
61
+ : normalized;
62
+ if (Object.hasOwn(CODEX_FAMILY_PROFILES, name))
63
+ definition = CODEX_FAMILY_PROFILES[name];
64
+ } else if (cli === "antigravity" || cli === "agy") {
65
+ const name = normalizeGeminiProfileName(profile);
66
+ if (Object.hasOwn(AGY_FAMILY_PROFILES, name))
67
+ definition = AGY_FAMILY_PROFILES[name];
68
+ }
69
+ return definition
70
+ ? { family: definition.family, effort: definition.effort }
71
+ : null;
72
+ }
73
+
74
+ export function profileFromModelEffort(cli, { family, effort }) {
75
+ const profiles =
76
+ cli === "codex"
77
+ ? CODEX_FAMILY_PROFILES
78
+ : cli === "antigravity" || cli === "agy"
79
+ ? AGY_FAMILY_PROFILES
80
+ : {};
81
+ return (
82
+ Object.entries(profiles).find(
83
+ ([, value]) => value.family === family && value.effort === effort,
84
+ )?.[0] || null
85
+ );
86
+ }
87
+
88
+ export function resolveCodexModelEffort({ family, effort }, opts = {}) {
89
+ const { families } = resolveModelFamilies("codex", {
90
+ ...opts,
91
+ allowCommand: false,
92
+ persist: false,
93
+ });
94
+ if (!Object.hasOwn(families, family) || !EFFORT_ORDER.includes(effort)) {
95
+ throw new Error(`알 수 없는 Codex 가족 또는 effort: ${family}/${effort}`);
96
+ }
97
+ const model = families[family].model;
98
+ const supported = readCodexModelCatalog(opts)?.models?.find(
99
+ (entry) => entry?.slug === model,
100
+ )?.supported_reasoning_levels;
101
+ if (!Array.isArray(supported)) return { family, model, effort };
102
+ const levels = supported.map((level) => level?.effort ?? level);
103
+ const selected = EFFORT_ORDER.slice(0, EFFORT_ORDER.indexOf(effort) + 1)
104
+ .filter((level) => levels.includes(level))
105
+ .at(-1);
106
+ if (!selected) {
107
+ const error = new Error(
108
+ `${model}: ${effort} 이하의 지원 effort가 없습니다`,
109
+ );
110
+ error.code = "unsupported-effort";
111
+ throw error;
112
+ }
113
+ return { family, model, effort: selected };
114
+ }
115
+
116
+ export function resolveAgyModelEffort({ family, effort }, opts = {}) {
117
+ const { families } = resolveModelFamilies("agy", opts);
118
+ if (!Object.hasOwn(families, family) || !EFFORT_ORDER.includes(effort)) {
119
+ throw new Error(`알 수 없는 agy 가족 또는 effort: ${family}/${effort}`);
120
+ }
121
+ const { model, supportedEfforts } = families[family];
122
+ if (!supportedEfforts) return { family, model, effort };
123
+ const selected = EFFORT_ORDER.slice(0, EFFORT_ORDER.indexOf(effort) + 1)
124
+ .filter((level) => supportedEfforts.includes(level))
125
+ .at(-1);
126
+ if (!selected) {
127
+ const error = new Error(
128
+ `${model}: ${effort} 이하의 지원 effort가 없습니다`,
129
+ );
130
+ error.code = "unsupported-effort";
131
+ throw error;
132
+ }
133
+ return { family, model, effort: selected };
134
+ }
@@ -39,7 +39,8 @@ export const AGY_FAMILY_PROFILES = Object.freeze({
39
39
  });
40
40
  const DAY_MS = 86_400_000;
41
41
  const CODEX_SLUG = /^gpt-(\d+(?:\.\d+)*)-(sol|astra|terra|luna)$/;
42
- const AGY_SLUG = /^gemini-(\d+(?:\.\d+)*)-(argon)-(low|medium|high)$/;
42
+ const AGY_SLUG =
43
+ /^gemini-(\d+(?:\.\d+)*)-(argon)-(none|low|medium|high|xhigh|max|ultra)$/;
43
44
 
44
45
  function compareVersions(a, b) {
45
46
  const left = a.split(".").map(Number);
@@ -59,10 +60,11 @@ export function selectCodexModel(models, family, effort) {
59
60
  return (
60
61
  match?.[2] === family &&
61
62
  item.visibility === "list" &&
62
- Array.isArray(item.supported_reasoning_levels) &&
63
- item.supported_reasoning_levels.some(
64
- (level) => (level?.effort ?? level) === effort,
65
- )
63
+ (effort === undefined ||
64
+ (Array.isArray(item.supported_reasoning_levels) &&
65
+ item.supported_reasoning_levels.some(
66
+ (level) => (level?.effort ?? level) === effort,
67
+ )))
66
68
  );
67
69
  })
68
70
  .sort((a, b) =>
@@ -89,7 +91,7 @@ export function selectAgyModel(models, family, effort) {
89
91
  const match = AGY_SLUG.exec(item?.slug);
90
92
  return (
91
93
  match?.[2] === family &&
92
- match?.[3] === effort &&
94
+ (effort === undefined || match?.[3] === effort) &&
93
95
  typeof item.model === "string"
94
96
  );
95
97
  })
@@ -203,7 +205,7 @@ export function resolveModelFamilies(provider, opts = {}) {
203
205
  if (provider === "codex") {
204
206
  const cachePath = join(paths.codexHome, "models_cache.json");
205
207
  sourceSignature = signature(cachePath);
206
- if (sourceSignature !== previous.sourceSignature)
208
+ if (sourceSignature !== previous.sourceSignature || !previous.families)
207
209
  catalog = readCodexModelCatalog(opts);
208
210
  if (
209
211
  sourceSignature.endsWith(":missing") &&
@@ -266,6 +268,8 @@ export function resolveModelFamilies(provider, opts = {}) {
266
268
  }
267
269
  const profiles = {};
268
270
  const selectedProfiles = {};
271
+ const families = {};
272
+ const selectedFamilies = {};
269
273
  const pins = readJson(paths.pinsPath) || {};
270
274
  for (const [name, definition] of Object.entries(definitions)) {
271
275
  const saved = previous.profiles?.[name];
@@ -329,13 +333,74 @@ export function resolveModelFamilies(provider, opts = {}) {
329
333
  fetched_at: null,
330
334
  };
331
335
  }
336
+ if (provider === "codex" && !Object.hasOwn(families, definition.family)) {
337
+ const latest = selectCodexModel(catalog?.models, definition.family);
338
+ const lastFamily =
339
+ previous.families?.[definition.family] || profiles[name];
340
+ families[definition.family] =
341
+ latest &&
342
+ (!lastFamily.fetched_at ||
343
+ !catalog.fetched_at ||
344
+ Date.parse(catalog.fetched_at) >= Date.parse(lastFamily.fetched_at))
345
+ ? { model: latest.slug, fetched_at: catalog.fetched_at || fetchedAt }
346
+ : { model: lastFamily.model, fetched_at: lastFamily.fetched_at };
347
+ selectedFamilies[definition.family] =
348
+ typeof pin === "string" &&
349
+ CODEX_SLUG.exec(pin)?.[2] === definition.family
350
+ ? { model: pin, source: paths.pinsPath, fetched_at: null }
351
+ : { ...families[definition.family] };
352
+ }
353
+ }
354
+ if (provider === "agy") {
355
+ const latest = selectAgyModel(catalog?.models, "argon");
356
+ // 옛 기록에는 가족 ID가 없으므로 마지막 성공 프로필의 표시 이름에서 복원한다.
357
+ const savedModels = [
358
+ ...Object.values(AGY_FAMILY_PROFILES),
359
+ ...Object.values(profiles),
360
+ ].flatMap((profile) => {
361
+ const match = /^Gemini (\d+(?:\.\d+)*) Argon \(/i.exec(profile.model);
362
+ return match
363
+ ? [{ ...profile, slug: `gemini-${match[1]}-argon-${profile.effort}` }]
364
+ : [];
365
+ });
366
+ const saved = selectAgyModel(savedModels, "argon");
367
+ const last = previous.families?.argon || {
368
+ model: saved.slug.replace(/-[^-]+$/, ""),
369
+ fetched_at: saved.fetched_at,
370
+ };
371
+ families.argon =
372
+ latest &&
373
+ (!last.fetched_at ||
374
+ !catalog.fetched_at ||
375
+ Date.parse(catalog.fetched_at) >= Date.parse(last.fetched_at))
376
+ ? {
377
+ model: latest.slug.replace(/-[^-]+$/, ""),
378
+ fetched_at: catalog.fetched_at || fetchedAt,
379
+ }
380
+ : last;
381
+ const pin = pins.argon;
382
+ selectedFamilies.argon =
383
+ typeof pin === "string" && AGY_SLUG.test(`${pin}-low`)
384
+ ? { model: pin, source: paths.pinsPath, fetched_at: null }
385
+ : { ...families.argon };
386
+ const supported = catalog?.models
387
+ ?.filter((item) =>
388
+ item.slug.startsWith(`${selectedFamilies.argon.model}-`),
389
+ )
390
+ .map((item) => AGY_SLUG.exec(item.slug)?.[3])
391
+ .filter(Boolean);
392
+ if (supported?.length) selectedFamilies.argon.supportedEfforts = supported;
332
393
  }
333
394
  const record = {
334
395
  sourceSignature,
335
396
  checked_at: checkedAt || null,
336
397
  profiles,
337
398
  selectedProfiles,
399
+ families,
338
400
  };
339
401
  if (opts.persist !== false) saveJson(recordPath, record);
340
- return { profiles: selectedProfiles };
402
+ return {
403
+ profiles: selectedProfiles,
404
+ families: selectedFamilies,
405
+ };
341
406
  }