minecodex 1.0.7 → 1.0.8

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.
@@ -16,13 +16,6 @@
16
16
  "modelSelector": {
17
17
  "maxVisibleItems": 6,
18
18
  "favoriteStorageKey": "codex-model-slider:favorites:v1",
19
- "reasoningEffortOverrides": {
20
- "command-code/Qwen-Qwen3.8-Flash": ["low", "medium", "xhigh"],
21
- "command-code/xai-grok-4.6": ["low", "medium", "high", "xhigh"],
22
- "command-code/xiaomi-mimo-v2.5": ["low", "medium", "high", "xhigh"],
23
- "opencode-go/hy3": ["low", "medium", "high"],
24
- "opencode-go/muse-spark-1.2-contributor": ["low", "medium", "high", "xhigh"]
25
- },
26
19
  "providerAliases": {
27
20
  "opencode-go": "OpenCode Go",
28
21
  "packyapi": "PackyAPI",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,47 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ // Model Slider 思考强度来源说明(为什么需要这个模块)
6
+ //
7
+ // OpenCodex 把第三方模型路由给 Codex 时,会在 catalog 里给所有“有思考强度”的
8
+ // 模型额外广告 mock 的 max/ultra(即使上游并不支持)。GLM 之类被 OpenCodex 测得
9
+ // 准的模型没有这个问题;而像 Qwen/Grok/MiMo 这类 OpenCodex 没内置档位的模型,
10
+ // 只能通过用户的 OpenCodex 配置声明真实档位。
11
+ //
12
+ // 这个模块只做一件事:读取 OpenCodex 配置(~/.opencodex/config.json)中按
13
+ // provider 声明的 modelReasoningEfforts,作为 Model Slider 的“真实档位表”。
14
+ // 它不产生、不猜测、不修改任何档位;配置里没有的模型保持原样。
15
+ function canonicalEfforts(values) {
16
+ if (!Array.isArray(values)) return null;
17
+ const seen = new Set();
18
+ const out = values
19
+ .map((value) => String(value).trim().toLowerCase())
20
+ .filter((value) => {
21
+ if (!value || seen.has(value)) return false;
22
+ seen.add(value);
23
+ return true;
24
+ });
25
+ return out.length ? out : null;
26
+ }
27
+
28
+ // 读取 OpenCodex 配置中按 provider 声明的真实思考强度档位。OpenCodex 同步 catalog
29
+ // 时会额外广告 mock 的 max/ultra;这里只透传 provider 自己声明的档位,供 Model
30
+ // Slider 精确显示,避免 UI 出现上游并不支持的档位。
31
+ export async function loadOpenCodexRealEfforts({
32
+ opencodexConfigPath = process.env.OPENCODEX_CONFIG_PATH ?? path.join(os.homedir(), ".opencodex", "config.json"),
33
+ read = readFile,
34
+ } = {}) {
35
+ const configPayload = await read(opencodexConfigPath, "utf8")
36
+ .then((value) => JSON.parse(value))
37
+ .catch(() => null);
38
+ const entries = {};
39
+ for (const [provider, providerConfig] of Object.entries(configPayload?.providers ?? {})) {
40
+ for (const [modelId, efforts] of Object.entries(providerConfig?.modelReasoningEfforts ?? {})) {
41
+ const levels = canonicalEfforts(efforts);
42
+ if (!levels) continue;
43
+ entries[`${provider}/${modelId}`.toLowerCase()] = levels;
44
+ }
45
+ }
46
+ return entries;
47
+ }
@@ -380,15 +380,11 @@ export function createInjectionSource(features, {
380
380
  [data-codex-model-slider-trigger] [class*="_ModelPickerTriggerEffortLabel_"] {
381
381
  display: inline-flex; align-items: center; align-self: center;
382
382
  }
383
- /* Fast 模式时,模型按钮 wrapper 居中,不再按有 Fast 的左对齐策略排布。 */
384
- [data-codex-model-slider-has-fast="false"] [data-codex-model-slider-model-button] {
385
- box-sizing: border-box; width: 100%; max-width: 100%; margin-inline: auto;
386
- flex: 0 1 100%; justify-content: center;
387
- }
388
- [data-codex-model-slider-has-fast="false"] [data-codex-model-slider-model-button] > span {
389
- width: 100%; min-width: 0;
390
- }
391
- [data-codex-model-slider-has-fast="false"] > :has(> [data-codex-model-slider-model-button])::before {
383
+ /* 模型按钮 wrapper 保持内容 hug,由原生 ViewControls
384
+ justify-content:center 相对面板全宽居中。原生 ::before 占位
385
+ (约 16px)会把 hug 内容推离中心,统一隐藏,让按钮中心与面板
386
+ 中心重合;Fast 按钮本身绝对定位于左缘不受影响。 */
387
+ [data-codex-model-slider-generic] > :has(> [data-codex-model-slider-model-button])::before {
392
388
  display: none !important; content: none !important;
393
389
  }
394
390
  html[data-codex-model-slider-selecting-effort] [role="menu"][data-state="open"]:not(:has([data-reasoning-slider])) {
@@ -856,7 +852,10 @@ export function createInjectionSource(features, {
856
852
  async function loadNativeFastIconFromBundle() {
857
853
  // 两个图标均已就绪(持久化或 DOM 克隆)时不再拉取整包 app-initial 资源。
858
854
  if (modelSelectorNativeFastIcons?.active && modelSelectorNativeFastIcons?.inactive) return;
859
- const sourceLink = document.querySelector('link[href*="/assets/app-initial-"][href$=".js"]');
855
+ // Fast bolt 图标定义在新版随 app-primary 入口包加载(app-initial 里没有),
856
+ // 按 app-primary -> app-initial 顺序取第一个已加载的入口包。
857
+ const sourceLink = document.querySelector('link[href*="/assets/app-primary-"][href$=".js"]')
858
+ ?? document.querySelector('link[href*="/assets/app-initial-"][href$=".js"]');
860
859
  if (!sourceLink?.href) return;
861
860
  try {
862
861
  const source = await fetch(sourceLink.href).then((response) => response.text());
@@ -876,12 +875,14 @@ export function createInjectionSource(features, {
876
875
  : `<path d="${path}" fill="currentColor" />`,
877
876
  }, { brand: true });
878
877
  if (!svg) return;
878
+ svg.setAttribute("width", "20");
879
+ svg.setAttribute("height", "20");
879
880
  modelSelectorNativeFastIcons[state] = svg;
880
881
  modelSelectorNativeFastIconSources[state] = "bundle";
881
882
  gained = true;
882
883
  };
883
884
  define("active", "M11\\.9125 21\\.4125", { viewBox: "0 0 24 24" });
884
- define("inactive", "M7\\.38 16\\.2207", { viewBox: "0 0 20 20", transform: "translate(2.43 1.609)" });
885
+ define("inactive", "M9\\.80999 17\\.8302", { viewBox: "0 0 20 20" });
885
886
  if (gained) {
886
887
  modelSelectorNativeFastIconsDirty = true;
887
888
  persistNativeFastIcons();
@@ -920,8 +921,11 @@ export function createInjectionSource(features, {
920
921
  if (!svg) return;
921
922
  const template = svg.cloneNode(true);
922
923
  template.removeAttribute("id");
923
- template.removeAttribute("width");
924
- template.removeAttribute("height");
924
+ // 统一呈现尺寸:保留原生 viewBox,固定 20px。DOM 克隆来源是
925
+ // 原生 active(24px)/inactive(20px) 两套 SVG,若不归一化,
926
+ // active 24px 放进 26px 的 Fast content 容器会显得大一圈。
927
+ template.setAttribute("width", "20");
928
+ template.setAttribute("height", "20");
925
929
  if (!modelSelectorNativeFastIcons) {
926
930
  modelSelectorNativeFastIcons = {};
927
931
  modelSelectorNativeFastIconSources = {};
@@ -931,7 +935,9 @@ export function createInjectionSource(features, {
931
935
  modelSelectorNativeFastIconsDirty = true;
932
936
  };
933
937
  capture("active", "M11.9125 21.4125");
934
- capture("inactive", "M7.38 16.2207");
938
+ // 现版 app 的 inactive bolt 为 20x20,路径以 M9.80999 开头;
939
+ // 旧的 M7.38 前缀已匹配不到任何图标,导致 inactive 缺失。
940
+ capture("inactive", "M9.80999 17.8302");
935
941
  }
936
942
  const ready = Boolean(
937
943
  modelSelectorNativeFastIcons?.active && modelSelectorNativeFastIcons?.inactive,
@@ -960,6 +966,10 @@ export function createInjectionSource(features, {
960
966
  if (!template) return null;
961
967
  const icon = template.cloneNode(true);
962
968
  icon.setAttribute("aria-hidden", "true");
969
+ // 出口统一规格化:无论缓存来自 DOM 克隆(24/20px)、bundle(20px)还是
970
+ // 旧版持久化数据,最终都以 20px 呈现,与原生 Fast bolt 视觉一致。
971
+ icon.setAttribute("width", "20");
972
+ icon.setAttribute("height", "20");
963
973
  const iconClass = nativeCssModuleClass("FastModeIcon");
964
974
  if (iconClass && !icon.classList.contains(iconClass)) icon.classList.add(iconClass);
965
975
  return icon;
@@ -1109,12 +1119,7 @@ export function createInjectionSource(features, {
1109
1119
  const nativeReasoningLevels = (model.supportedReasoningEfforts ?? []).map((level) => ({
1110
1120
  effort: String(level?.reasoningEffort ?? level?.effort ?? level ?? "").toLowerCase(),
1111
1121
  })).filter((level) => level.effort);
1112
- const allowed = selector?.reasoningEffortOverrides?.[identity.raw.toLowerCase()];
1113
- const supportedReasoningLevels = allowed
1114
- ? (nativeReasoningLevels.length
1115
- ? nativeReasoningLevels.filter((level) => allowed.includes(level.effort))
1116
- : allowed.map((effort) => ({ effort })))
1117
- : nativeReasoningLevels;
1122
+ const supportedReasoningLevels = nativeReasoningLevels;
1118
1123
  const currentReasoningLevel = String(controller?.reasoningEffort ?? "").toLowerCase();
1119
1124
  return {
1120
1125
  slug: String(model.model ?? ""),
@@ -1134,6 +1139,31 @@ export function createInjectionSource(features, {
1134
1139
  return nativeCatalogModelFor(identity) ?? catalogModelFor(identity, selector);
1135
1140
  }
1136
1141
 
1142
+ // 用 OpenCodex 配置声明的真实档位对滑条做精确交集,过滤 catalog 为支持
1143
+ // mock 顶档而额外广告的 max/ultra。键匹配顺序:模型自带的
1144
+ // opencodex_capability_provenance → catalog 模型 → 菜单原始 identity;
1145
+ // 只做过滤不扩展,配置未声明的模型保持 catalog 原样。
1146
+ function modelWithOpenCodexEfforts(identity, selector, model) {
1147
+ if (!model || !selector?.openCodexRealEfforts) return model;
1148
+ const provenance = model.opencodex_capability_provenance;
1149
+ const catalogModel = provenance?.provider && provenance?.model_id
1150
+ ? null
1151
+ : catalogModelFor(identity, selector);
1152
+ const source = catalogModel ?? model;
1153
+ const sourceProvenance = source.opencodex_capability_provenance;
1154
+ const key = sourceProvenance?.provider && sourceProvenance?.model_id
1155
+ ? `${sourceProvenance.provider}/${sourceProvenance.model_id}`.toLowerCase()
1156
+ : identity.raw.toLowerCase();
1157
+ const realEfforts = selector.openCodexRealEfforts[key];
1158
+ if (!realEfforts) return model;
1159
+ const allowed = new Set(realEfforts);
1160
+ const levels = (model.supportedReasoningLevels ?? []).filter((level) => {
1161
+ const effort = String(level?.effort ?? level ?? "").toLowerCase();
1162
+ return allowed.has(effort);
1163
+ });
1164
+ return { ...model, supportedReasoningLevels: levels };
1165
+ }
1166
+
1137
1167
  function nativeModelDescriptorForItem(item, observedRaw) {
1138
1168
  const target = comparableModelLabel(observedRaw);
1139
1169
  if (!target) return null;
@@ -1184,19 +1214,6 @@ export function createInjectionSource(features, {
1184
1214
  return formatIdentifier(identity.backend).toLowerCase() === nativeLabel ? native : thirdParty;
1185
1215
  }
1186
1216
 
1187
- function visibleReasoningLevelsFor(model) {
1188
- if (!model) return [];
1189
- const capability = model.modelSelectorCapability;
1190
- if (capability && Array.isArray(capability.levels)) {
1191
- const allowed = new Set(capability.levels);
1192
- return (model.supportedReasoningLevels ?? []).filter((level) => {
1193
- const effort = String(level?.effort ?? level ?? "").toLowerCase();
1194
- return allowed.has(effort);
1195
- });
1196
- }
1197
- return model.supportedReasoningLevels ?? [];
1198
- }
1199
-
1200
1217
  function loadModelFavorites(selector) {
1201
1218
  try {
1202
1219
  const value = JSON.parse(localStorage.getItem(selector.favoriteStorageKey) ?? "[]");
@@ -2412,23 +2429,8 @@ export function createInjectionSource(features, {
2412
2429
  function enhanceGenericModelPicker(parentMenu, selector) {
2413
2430
  const identity = modelIdentity(nativeModelValue(parentMenu));
2414
2431
  const model = modelDefinitionFor(identity, selector);
2415
- const catalogModel = catalogModelFor(identity, selector);
2416
- const catalogedLevels = model?.supportedReasoningLevels ?? catalogModel?.supportedReasoningLevels ?? [];
2417
- // 外部模型在菜单中的 identity 常是 displayName(如 commandcode-auth/xai-grok-4.6),
2418
- // 而 reasoningEffortOverrides 按 catalog slug(command-code/xai-grok-4.6)声明;
2419
- // 先按 catalog slug 匹配,再退回菜单原始值,确保档位兜底能命中。
2420
- const overrideLevels = selector?.reasoningEffortOverrides?.[catalogModel?.slug.toLowerCase()]
2421
- ?? selector?.reasoningEffortOverrides?.[identity.raw.toLowerCase()];
2422
- const modelWithCapability = model?.modelSelectorCapability ? model : {
2423
- ...(model ?? {}),
2424
- ...(catalogModel ?? {}),
2425
- // 外部模型 catalog 的 supported_reasoning_levels 可能为空;此时用
2426
- // modelSelector.reasoningEffortOverrides 补齐档位,否则滑条没有刻度。
2427
- supportedReasoningLevels: catalogedLevels.length
2428
- ? catalogedLevels
2429
- : (overrideLevels?.map((effort) => ({ effort })) ?? []),
2430
- };
2431
- const efforts = visibleReasoningLevelsFor(modelWithCapability);
2432
+ const displayModel = modelWithOpenCodexEfforts(identity, selector, model);
2433
+ const efforts = displayModel?.supportedReasoningLevels ?? [];
2432
2434
  if (!model) return false;
2433
2435
  // 控制器推导的任务内实际 effort 优先,避免子任务/主任务切换后触发器属性滞后;
2434
2436
  // 触发器属性仅在控制器无法解析时作为实时回退,静态目录默认值最后兜底。
@@ -2477,13 +2479,11 @@ export function createInjectionSource(features, {
2477
2479
  existing?.remove();
2478
2480
  const nativeContainer = parentMenu.firstElementChild;
2479
2481
  if (!nativeContainer) return false;
2480
- // 使用聚合了 override 兜底档位的 modelWithCapability,空目录档位的外部
2481
- // 模型才能渲染滑条刻度;原始 model 只负责身份/默认值。
2482
2482
  const shell = createGenericModelSlider(
2483
2483
  parentMenu,
2484
2484
  selector,
2485
2485
  identity,
2486
- modelWithCapability,
2486
+ displayModel,
2487
2487
  selectedEffortValue,
2488
2488
  );
2489
2489
  if (!shell) return false;
@@ -120,18 +120,6 @@ async function normalizeModelSelector(modelSelector, projectDir) {
120
120
  throw new Error("modelSelector.favoriteStorageKey is required");
121
121
  }
122
122
 
123
- const reasoningEffortOverrides = {};
124
- for (const [identity, efforts] of Object.entries(modelSelector.reasoningEffortOverrides ?? {})) {
125
- const exactIdentity = String(identity).trim().toLowerCase();
126
- const allowedEfforts = Array.isArray(efforts)
127
- ? efforts.map((effort) => String(effort).trim().toLowerCase()).filter(Boolean)
128
- : [];
129
- if (!exactIdentity || !allowedEfforts.length || new Set(allowedEfforts).size !== allowedEfforts.length) {
130
- throw new Error("modelSelector.reasoningEffortOverrides must map exact identities to unique efforts");
131
- }
132
- reasoningEffortOverrides[exactIdentity] = allowedEfforts;
133
- }
134
-
135
123
  const providerAliases = {};
136
124
  for (const [provider, label] of Object.entries(modelSelector.providerAliases ?? {})) {
137
125
  if (!String(provider).trim() || !String(label).trim()) {
@@ -165,7 +153,6 @@ async function normalizeModelSelector(modelSelector, projectDir) {
165
153
  return {
166
154
  maxVisibleItems,
167
155
  favoriteStorageKey,
168
- reasoningEffortOverrides,
169
156
  providerAliases,
170
157
  strings,
171
158
  icons: {
@@ -367,6 +354,7 @@ export async function loadConfiguredModelCatalog(
367
354
  ].every((value) => typeof value === "string" && value.trim())
368
355
  ? `${model.opencodex_capability_provenance.provider.trim()}/${model.opencodex_capability_provenance.model_id.trim()}`
369
356
  : null,
357
+ opencodex_capability_provenance: model.opencodex_capability_provenance ?? null,
370
358
  })).filter((model) => model.slug);
371
359
  } catch (error) {
372
360
  if (error.code === "ENOENT") return [];
@@ -11,7 +11,7 @@ import {
11
11
  selectEnabledFeatures,
12
12
  startFeatureProcesses,
13
13
  } from "./feature-registry.mjs";
14
- import { applyVisibleReasoningLevels, loadModelCapabilities } from "./model-capabilities.mjs";
14
+ import { loadOpenCodexRealEfforts } from "./codex-efforts.mjs";
15
15
 
16
16
  const runtimeRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
17
17
  const featuresRoot = process.env.CODEX_FEATURES_ROOT ?? path.dirname(runtimeRoot);
@@ -23,16 +23,14 @@ const discoveredFeatures = await discoverFeatures(featuresRoot);
23
23
  if (!discoveredFeatures.length) throw new Error(`No codex-feature.json files found under ${featuresRoot}`);
24
24
  const features = selectEnabledFeatures(discoveredFeatures, process.env.MINECODEX_ENABLED_FEATURES);
25
25
  const modelCatalog = await loadConfiguredModelCatalog();
26
- const modelCapabilities = await loadModelCapabilities();
26
+ // OpenCodex 配置声明的真实档位为准,避免 catalog 里 mock 出的 max/ultra
27
+ // 出现在 Model Slider 中。后续新增模型无需改这里:在 OpenCodex 配置的
28
+ // providers.<id>.modelReasoningEfforts 里补一行即可,启动后自动生效。
29
+ const openCodexRealEfforts = await loadOpenCodexRealEfforts();
27
30
  for (const feature of features) {
28
31
  if (feature.modelSelector) {
29
- feature.modelSelector.models = modelCatalog.map((model) => (
30
- applyVisibleReasoningLevels(
31
- model,
32
- modelCapabilities.get(String(model.capabilityKey ?? model.slug ?? "").toLowerCase()),
33
- model.supportedReasoningLevels,
34
- )
35
- ));
32
+ feature.modelSelector.models = modelCatalog;
33
+ feature.modelSelector.openCodexRealEfforts = openCodexRealEfforts;
36
34
  }
37
35
  }
38
36
 
@@ -1,86 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
-
5
- const CODEX_REASONING_ORDER = ["low", "medium", "high", "xhigh", "max", "ultra"];
6
-
7
- function uniqueLevels(values) {
8
- return [...new Set(values.filter(Boolean))];
9
- }
10
-
11
-
12
- function normalizedEfforts(entry) {
13
- if (!Array.isArray(entry)) return null;
14
- const levels = entry.map(String).filter((value) => (
15
- CODEX_REASONING_ORDER.includes(value) || value === "none" || value === "minimal"
16
- ));
17
- return levels.length ? uniqueLevels(levels) : null;
18
- }
19
-
20
- export function resolveModelRecord(records, modelId) {
21
- if (!records || typeof records !== "object") return undefined;
22
- if (Object.prototype.hasOwnProperty.call(records, modelId)) return records[modelId];
23
- const folded = modelId.toLowerCase();
24
- for (const [key, value] of Object.entries(records)) {
25
- if (key.toLowerCase() === folded) return value;
26
- }
27
- return undefined;
28
- }
29
-
30
- export function mergeCapability(providerConfig, modelId) {
31
- if (!providerConfig) return null;
32
- const configuredEfforts = normalizedEfforts(
33
- resolveModelRecord(providerConfig.modelReasoningEfforts, modelId),
34
- );
35
- if (configuredEfforts) {
36
- return {
37
- source: "opencodex",
38
- kind: "effort",
39
- levels: uniqueLevels(configuredEfforts),
40
- map: resolveModelRecord(providerConfig.modelReasoningEffortMap, modelId) ?? null,
41
- rawEfforts: configuredEfforts,
42
- };
43
- }
44
- if (Array.isArray(providerConfig.noReasoningModels) && providerConfig.noReasoningModels.includes(modelId)) {
45
- return { source: "opencodex", kind: "no-effort", levels: null, map: null, rawEfforts: [] };
46
- }
47
- return null;
48
- }
49
-
50
- export async function loadModelCapabilities({
51
- opencodexConfigPath = process.env.OPENCODEX_CONFIG_PATH ?? path.join(os.homedir(), ".opencodex", "config.json"),
52
- read = readFile,
53
- } = {}) {
54
- const configPayload = await read(opencodexConfigPath, "utf8").then((value) => JSON.parse(value)).catch(() => null);
55
- const providers = configPayload?.providers ?? {};
56
- const result = new Map();
57
- for (const [providerId, providerConfig] of Object.entries(providers)) {
58
- const modelIds = new Set([
59
- ...Object.keys(providerConfig?.modelReasoningEfforts ?? {}),
60
- ...Object.keys(providerConfig?.modelReasoningEffortMap ?? {}),
61
- ...Object.keys(providerConfig?.thinkingToggleModels ?? {}),
62
- ...Object.keys(providerConfig?.thinkingBudgetModels ?? {}),
63
- ...(Array.isArray(providerConfig?.noReasoningModels) ? providerConfig.noReasoningModels : []),
64
- ]);
65
- for (const modelId of modelIds) {
66
- const capability = mergeCapability(providerConfig, modelId);
67
- if (capability) result.set(`${providerId}/${modelId}`.toLowerCase(), capability);
68
- }
69
- }
70
- return result;
71
- }
72
-
73
- export function applyVisibleReasoningLevels(catalogModel, capability, fallbackLevels) {
74
- if (!catalogModel) return catalogModel;
75
- const rawLevels = Array.isArray(fallbackLevels) ? fallbackLevels : (catalogModel.supportedReasoningLevels ?? []);
76
- if (!Array.isArray(rawLevels)) return catalogModel;
77
- if (capability && Array.isArray(capability.levels)) {
78
- const allowed = new Set(capability.levels);
79
- const visible = rawLevels.filter((level) => {
80
- const effort = String(level?.effort ?? level ?? "").toLowerCase();
81
- return allowed.has(effort);
82
- });
83
- return { ...catalogModel, supportedReasoningLevels: visible, modelSelectorCapability: capability };
84
- }
85
- return { ...catalogModel, modelSelectorCapability: capability ?? null };
86
- }