@triflux/remote 10.87.0 → 10.88.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,4 +1,381 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import {
4
+ chmodSync,
5
+ copyFileSync,
6
+ existsSync,
7
+ readFileSync,
8
+ realpathSync,
9
+ renameSync,
10
+ unlinkSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+
14
+ const defaultFs = {
15
+ chmodSync,
16
+ copyFileSync,
17
+ existsSync,
18
+ readFileSync,
19
+ realpathSync,
20
+ renameSync,
21
+ unlinkSync,
22
+ writeFileSync,
23
+ };
24
+
25
+ // ARM64 Mach-O agy 바이너리의 time.After 함수와 유휴 대기 호출부 시그니처 (#1022 peer-wake 패치)
26
+ export const AGY_TIME_AFTER_SIG = Buffer.from(
27
+ "900b40f9ff6330eb29010054fe0f1ef8fd831ff8fd2300d16affff97000040f9fd835ff8fe0742f8c0035fd6",
28
+ "hex",
29
+ );
30
+ export const AGY_PEER_WAKE_ORIG_CALL = Buffer.from(
31
+ "e13740f9220041a9423040f9210c40a9e40302aae20303aa80003fd6",
32
+ "hex",
33
+ );
34
+ export const AGY_PEER_WAKE_PATCH_PREFIX = Buffer.from(
35
+ "1f2003d51f2003d51f2003d51f2003d5004099524073a772",
36
+ "hex",
37
+ );
38
+ export const AGY_PEER_WAKE_ORIG_STP = Buffer.from("e08708a9", "hex");
39
+ export const AGY_PEER_WAKE_PATCH_STP = Buffer.from("e0ff08a9", "hex");
40
+ const AGY_PEER_WAKE_STP_DELTA = 0x38;
41
+
42
+ export function encodeArm64Bl(pcOffset, targetOffset) {
43
+ const delta = targetOffset - pcOffset;
44
+ if (delta % 4 !== 0 || delta < -0x8000000 || delta > 0x7fffffc) {
45
+ return null;
46
+ }
47
+ const imm26 = (delta >> 2) & 0x03ffffff;
48
+ const out = Buffer.alloc(4);
49
+ out.writeUInt32LE((0x94000000 | imm26) >>> 0, 0);
50
+ return out;
51
+ }
52
+
53
+ function findUniqueOffset(buf, pattern) {
54
+ const first = buf.indexOf(pattern);
55
+ if (first === -1) return -1;
56
+ if (buf.indexOf(pattern, first + 1) !== -1) return -2;
57
+ return first;
58
+ }
59
+
60
+ export function inspectAgyPeerWakePatch({
61
+ agyPath,
62
+ platform = process.platform,
63
+ arch = process.arch,
64
+ fs = defaultFs,
65
+ } = {}) {
66
+ if (platform !== "darwin" || arch !== "arm64") {
67
+ return { ok: true, status: "skipped", platform, arch };
68
+ }
69
+ if (!agyPath) {
70
+ return { ok: true, status: "skipped", reason: "agy-not-installed" };
71
+ }
72
+ let resolvedPath = agyPath;
73
+ try {
74
+ resolvedPath = fs.realpathSync(agyPath);
75
+ } catch {
76
+ return { ok: true, status: "skipped", reason: "agy-not-found" };
77
+ }
78
+ if (!fs.existsSync(resolvedPath)) {
79
+ return { ok: true, status: "skipped", reason: "agy-not-found" };
80
+ }
81
+
82
+ const patchJsonPath = `${resolvedPath}.patch.json`;
83
+ let buf;
84
+ try {
85
+ buf = fs.readFileSync(resolvedPath);
86
+ } catch (error) {
87
+ return {
88
+ ok: false,
89
+ status: "error",
90
+ patchable: false,
91
+ path: resolvedPath,
92
+ patchJsonPath,
93
+ reason: "read-failed",
94
+ message: error?.message || String(error),
95
+ };
96
+ }
97
+ const sha256 = createHash("sha256").update(buf).digest("hex");
98
+ const timeAfterOffset = findUniqueOffset(buf, AGY_TIME_AFTER_SIG);
99
+ if (timeAfterOffset < 0) {
100
+ return {
101
+ ok: false,
102
+ status: "unsupported",
103
+ patchable: false,
104
+ path: resolvedPath,
105
+ patchJsonPath,
106
+ sha256,
107
+ reason:
108
+ timeAfterOffset === -2
109
+ ? "ambiguous-time-after-signature"
110
+ : "time-after-signature-not-found",
111
+ };
112
+ }
113
+
114
+ const patchedOffset = findUniqueOffset(buf, AGY_PEER_WAKE_PATCH_PREFIX);
115
+ if (
116
+ patchedOffset >= 0 &&
117
+ patchedOffset + AGY_PEER_WAKE_STP_DELTA + 4 <= buf.length
118
+ ) {
119
+ const expectedBl = encodeArm64Bl(
120
+ patchedOffset + AGY_PEER_WAKE_PATCH_PREFIX.length,
121
+ timeAfterOffset,
122
+ );
123
+ const actualBl = buf.subarray(
124
+ patchedOffset + AGY_PEER_WAKE_PATCH_PREFIX.length,
125
+ patchedOffset + AGY_PEER_WAKE_PATCH_PREFIX.length + 4,
126
+ );
127
+ const actualStp = buf.subarray(
128
+ patchedOffset + AGY_PEER_WAKE_STP_DELTA,
129
+ patchedOffset + AGY_PEER_WAKE_STP_DELTA + 4,
130
+ );
131
+ if (
132
+ expectedBl &&
133
+ actualBl.equals(expectedBl) &&
134
+ actualStp.equals(AGY_PEER_WAKE_PATCH_STP)
135
+ ) {
136
+ let metadataOk = false;
137
+ if (fs.existsSync(patchJsonPath)) {
138
+ try {
139
+ const meta = JSON.parse(fs.readFileSync(patchJsonPath, "utf8"));
140
+ metadataOk =
141
+ meta?.sha256 === sha256 &&
142
+ Array.isArray(meta?.patches) &&
143
+ meta.patches.includes("peer-wake");
144
+ } catch {
145
+ metadataOk = false;
146
+ }
147
+ }
148
+ if (metadataOk) {
149
+ return {
150
+ ok: true,
151
+ status: "patched",
152
+ patchable: false,
153
+ binaryPatched: true,
154
+ path: resolvedPath,
155
+ patchJsonPath,
156
+ sha256,
157
+ callOffset: patchedOffset,
158
+ timeAfterOffset,
159
+ };
160
+ }
161
+ return {
162
+ ok: false,
163
+ status: "metadata-outdated",
164
+ patchable: true,
165
+ binaryPatched: true,
166
+ path: resolvedPath,
167
+ patchJsonPath,
168
+ sha256,
169
+ callOffset: patchedOffset,
170
+ timeAfterOffset,
171
+ fix: "tfx doctor --fix",
172
+ };
173
+ }
174
+ }
175
+
176
+ const origOffset = findUniqueOffset(buf, AGY_PEER_WAKE_ORIG_CALL);
177
+ if (
178
+ origOffset >= 0 &&
179
+ origOffset + AGY_PEER_WAKE_STP_DELTA + 4 <= buf.length
180
+ ) {
181
+ const expectedBl = encodeArm64Bl(
182
+ origOffset + AGY_PEER_WAKE_PATCH_PREFIX.length,
183
+ timeAfterOffset,
184
+ );
185
+ const actualStp = buf.subarray(
186
+ origOffset + AGY_PEER_WAKE_STP_DELTA,
187
+ origOffset + AGY_PEER_WAKE_STP_DELTA + 4,
188
+ );
189
+ if (expectedBl && actualStp.equals(AGY_PEER_WAKE_ORIG_STP)) {
190
+ return {
191
+ ok: false,
192
+ status: "unpatched",
193
+ patchable: true,
194
+ binaryPatched: false,
195
+ path: resolvedPath,
196
+ patchJsonPath,
197
+ sha256,
198
+ callOffset: origOffset,
199
+ timeAfterOffset,
200
+ fix: "tfx doctor --fix",
201
+ };
202
+ }
203
+ }
204
+
205
+ return {
206
+ ok: false,
207
+ status: "unsupported",
208
+ patchable: false,
209
+ path: resolvedPath,
210
+ patchJsonPath,
211
+ sha256,
212
+ reason:
213
+ origOffset === -2
214
+ ? "ambiguous-call-site-signature"
215
+ : "call-site-signature-not-found",
216
+ };
217
+ }
218
+
219
+ export function applyAgyPeerWakePatch({
220
+ agyPath,
221
+ version,
222
+ platform = process.platform,
223
+ arch = process.arch,
224
+ fs = defaultFs,
225
+ execFileSyncFn = execFileSync,
226
+ } = {}) {
227
+ const state = inspectAgyPeerWakePatch({ agyPath, platform, arch, fs });
228
+ if (state.status === "patched" || state.status === "skipped") {
229
+ return { ...state, changed: false };
230
+ }
231
+ if (!state.patchable) {
232
+ return { ...state, ok: false, changed: false };
233
+ }
234
+
235
+ let resolvedVersion = version || "";
236
+ if (!resolvedVersion) {
237
+ try {
238
+ const out = String(
239
+ execFileSyncFn(state.path, ["--version"], {
240
+ encoding: "utf8",
241
+ timeout: 5000,
242
+ stdio: ["ignore", "pipe", "ignore"],
243
+ }) || "",
244
+ );
245
+ resolvedVersion = out.match(/\b(\d+\.\d+\.\d+)\b/)?.[1] || "unknown";
246
+ } catch {
247
+ resolvedVersion = "unknown";
248
+ }
249
+ }
250
+
251
+ let finalSha256 = state.sha256;
252
+ let backupEntry = null;
253
+ if (state.status === "unpatched") {
254
+ const rawBuf = fs.readFileSync(state.path);
255
+ const currentSha256 = createHash("sha256").update(rawBuf).digest("hex");
256
+ if (currentSha256 !== state.sha256) {
257
+ throw new Error("agy binary changed during patch preparation");
258
+ }
259
+
260
+ let backupKey = `${resolvedVersion}-orig`;
261
+ let backupPath = `${state.path}.bak-${backupKey}`;
262
+ if (fs.existsSync(backupPath)) {
263
+ const existingBackupSha = createHash("sha256")
264
+ .update(fs.readFileSync(backupPath))
265
+ .digest("hex");
266
+ if (existingBackupSha !== state.sha256) {
267
+ backupKey = `${resolvedVersion}-orig-${state.sha256.slice(0, 8)}`;
268
+ backupPath = `${state.path}.bak-${backupKey}`;
269
+ if (fs.existsSync(backupPath)) {
270
+ const suffixBackupSha = createHash("sha256")
271
+ .update(fs.readFileSync(backupPath))
272
+ .digest("hex");
273
+ if (suffixBackupSha !== state.sha256) {
274
+ throw new Error(`agy backup conflict: ${backupPath}`);
275
+ }
276
+ } else {
277
+ fs.copyFileSync(state.path, backupPath);
278
+ }
279
+ }
280
+ } else {
281
+ fs.copyFileSync(state.path, backupPath);
282
+ }
283
+ backupEntry = { key: backupKey, path: backupPath, sha256: state.sha256 };
284
+
285
+ const buf = Buffer.from(rawBuf);
286
+ const expectedBl = encodeArm64Bl(
287
+ state.callOffset + AGY_PEER_WAKE_PATCH_PREFIX.length,
288
+ state.timeAfterOffset,
289
+ );
290
+ AGY_PEER_WAKE_PATCH_PREFIX.copy(buf, state.callOffset);
291
+ expectedBl.copy(buf, state.callOffset + AGY_PEER_WAKE_PATCH_PREFIX.length);
292
+ AGY_PEER_WAKE_PATCH_STP.copy(
293
+ buf,
294
+ state.callOffset + AGY_PEER_WAKE_STP_DELTA,
295
+ );
296
+
297
+ const tmpPath = `${state.path}.tmp-${process.pid}`;
298
+ fs.writeFileSync(tmpPath, buf, { mode: 0o755 });
299
+ try {
300
+ fs.chmodSync(tmpPath, 0o755);
301
+ execFileSyncFn("codesign", ["-f", "-s", "-", tmpPath], {
302
+ stdio: "ignore",
303
+ timeout: 30000,
304
+ });
305
+ const preRenameSha256 = createHash("sha256")
306
+ .update(fs.readFileSync(state.path))
307
+ .digest("hex");
308
+ if (preRenameSha256 !== state.sha256) {
309
+ throw new Error("agy binary changed before atomic replace");
310
+ }
311
+ fs.renameSync(tmpPath, state.path);
312
+ } catch (error) {
313
+ try {
314
+ fs.unlinkSync(tmpPath);
315
+ } catch {}
316
+ throw error;
317
+ }
318
+ finalSha256 = createHash("sha256")
319
+ .update(fs.readFileSync(state.path))
320
+ .digest("hex");
321
+ }
322
+
323
+ let existingMeta = {};
324
+ if (fs.existsSync(state.patchJsonPath)) {
325
+ try {
326
+ const parsed = JSON.parse(fs.readFileSync(state.patchJsonPath, "utf8"));
327
+ existingMeta =
328
+ parsed && typeof parsed === "object" && !Array.isArray(parsed)
329
+ ? parsed
330
+ : {};
331
+ } catch {
332
+ existingMeta = {};
333
+ }
334
+ }
335
+ const patches = Array.isArray(existingMeta.patches)
336
+ ? [...new Set([...existingMeta.patches, "peer-wake"])]
337
+ : ["peer-wake"];
338
+ const backups = {
339
+ ...(backupEntry
340
+ ? {
341
+ [backupEntry.key]: {
342
+ path: backupEntry.path,
343
+ sha256: backupEntry.sha256,
344
+ },
345
+ }
346
+ : {}),
347
+ ...(existingMeta.backups && typeof existingMeta.backups === "object"
348
+ ? existingMeta.backups
349
+ : {}),
350
+ };
351
+ const nextMeta = {
352
+ ...existingMeta,
353
+ version:
354
+ resolvedVersion !== "unknown"
355
+ ? resolvedVersion
356
+ : existingMeta.version || "unknown",
357
+ sha256: finalSha256,
358
+ patches,
359
+ appliedAt: new Date().toISOString(),
360
+ backups,
361
+ };
362
+ fs.writeFileSync(
363
+ state.patchJsonPath,
364
+ `${JSON.stringify(nextMeta, null, 2)}\n`,
365
+ "utf8",
366
+ );
367
+
368
+ return {
369
+ ok: true,
370
+ status: "patched",
371
+ changed: true,
372
+ binaryPatched: true,
373
+ previousStatus: state.status,
374
+ path: state.path,
375
+ patchJsonPath: state.patchJsonPath,
376
+ sha256: finalSha256,
377
+ };
378
+ }
2
379
 
3
380
  export function commandExists(name) {
4
381
  try {
@@ -1,6 +1,6 @@
1
1
  import { copyFileSync, existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import { normalizeGeminiProfileName } from "@triflux/core/scripts/lib/model-effort.mjs";
3
+ import { normalizeGeminiProfileName } from "./model-effort.mjs";
4
4
  import {
5
5
  AGY_FAMILY_PROFILES,
6
6
  modelFamilyPaths,
@@ -5,6 +5,7 @@
5
5
  import { readFileSync } from "node:fs";
6
6
  import process from "node:process";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { isJevEnabled, judgeMcpSelection } from "@triflux/core/hub/lib/jev-addon.mjs";
8
9
  import { CORE_SERVERS, readManifest } from "./mcp-manifest.mjs";
9
10
  import {
10
11
  DOMAIN_TAG_KEYWORDS,
@@ -639,7 +640,11 @@ export function getCodexMcpConfig(options = {}) {
639
640
  if (registeredServers.length === 0) {
640
641
  return { mcp_servers: {} };
641
642
  }
642
- const allowedServers = new Set(resolveAllowedServers(options));
643
+ const allowedServers = new Set(
644
+ Array.isArray(options.allowedServers)
645
+ ? parseAvailableServers(options.allowedServers)
646
+ : resolveAllowedServers(options),
647
+ );
643
648
  const resolvedProfile = resolveMcpProfile(
644
649
  options.agentType,
645
650
  options.requestedProfile,
@@ -731,9 +736,17 @@ export function buildMcpPolicy(options = {}) {
731
736
  const hint = buildPromptHint(resolvedOptions);
732
737
 
733
738
  // 플러그인 서버는 transport 설정이 없어 CLI override를 받으면 부팅이 실패한다.
739
+ const configServers = codexServersFromConfig(options.codexConfig);
740
+ const registeredServers =
741
+ configServers.length > 0
742
+ ? configServers
743
+ : options.registeredServers !== undefined
744
+ ? parseAvailableServers(options.registeredServers)
745
+ : [];
734
746
  const codexOptions = {
735
747
  ...resolvedOptions,
736
- availableServers: codexServersFromConfig(options.codexConfig),
748
+ availableServers: registeredServers,
749
+ allowedServers,
737
750
  };
738
751
 
739
752
  return {
@@ -749,6 +762,74 @@ export function buildMcpPolicy(options = {}) {
749
762
  };
750
763
  }
751
764
 
765
+ export async function buildMcpPolicyWithJev(options = {}, jevOptions = {}) {
766
+ const basePolicy = buildMcpPolicy(options);
767
+ if (!isJevEnabled(jevOptions)) return basePolicy;
768
+ const taskText = String(options.taskText || "").trim();
769
+ if (!taskText) return basePolicy;
770
+
771
+ const isAutoProfile =
772
+ !options.requestedProfile || options.requestedProfile === "auto";
773
+ if (!isAutoProfile && options.requestedProfile === "none") {
774
+ return basePolicy;
775
+ }
776
+
777
+ const candidateServers = basePolicy.allowedServers;
778
+ if (candidateServers.length === 0) return basePolicy;
779
+
780
+ const selection = await judgeMcpSelection(
781
+ {
782
+ taskText,
783
+ agentType: options.agentType || basePolicy.resolvedProfile,
784
+ availableServers: candidateServers,
785
+ },
786
+ jevOptions,
787
+ );
788
+ if (!selection || !Array.isArray(selection.neededMcpServers))
789
+ return basePolicy;
790
+
791
+ const narrowedAvailable = selection.neededMcpServers.filter((server) =>
792
+ basePolicy.allowedServers.includes(server),
793
+ );
794
+ if (narrowedAvailable.length === 0) {
795
+ return {
796
+ ...buildMcpPolicy({
797
+ ...options,
798
+ requestedProfile: "none",
799
+ registeredServers:
800
+ options.registeredServers ??
801
+ (options.codexConfig === undefined
802
+ ? options.availableServers
803
+ : undefined),
804
+ }),
805
+ requestedProfile: options.requestedProfile || "auto",
806
+ };
807
+ }
808
+
809
+ const nextProfile =
810
+ isAutoProfile &&
811
+ selection.mcpProfile &&
812
+ selection.mcpProfile in PROFILE_DEFINITIONS
813
+ ? selection.mcpProfile
814
+ : options.requestedProfile;
815
+
816
+ const resolved = buildMcpPolicy({
817
+ ...options,
818
+ requestedProfile: nextProfile,
819
+ availableServers: narrowedAvailable,
820
+ registeredServers:
821
+ options.registeredServers ??
822
+ (options.codexConfig === undefined
823
+ ? options.availableServers
824
+ : undefined),
825
+ });
826
+ return {
827
+ ...resolved,
828
+ enabledServers: resolved.allowedServers,
829
+ requestedProfile: options.requestedProfile || "auto",
830
+ };
831
+ }
832
+
752
833
  function shellEscape(value) {
753
834
  return `'${String(value).replace(/'/g, `'"'"'`)}'`;
754
835
  }
@@ -863,7 +944,9 @@ async function runCli(argv = process.argv.slice(2)) {
863
944
 
864
945
  let policy;
865
946
  try {
866
- policy = buildMcpPolicy(args);
947
+ policy = isJevEnabled()
948
+ ? await buildMcpPolicyWithJev(args)
949
+ : buildMcpPolicy(args);
867
950
  } catch (error) {
868
951
  console.error(
869
952
  `[mcp-filter] ${error instanceof Error ? error.message : String(error)}`,
@@ -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
+ }