@genee/omp-opsx-addon 0.7.0 → 0.9.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/lib/tiers-data.ts CHANGED
@@ -8,7 +8,20 @@
8
8
  import { readFileSync } from 'node:fs';
9
9
  import { homedir } from 'node:os';
10
10
  import { join } from 'node:path';
11
- import type { TierName } from './model-tiers.js';
11
+ import { TIER_RANK, rankToTierName, type TierName } from './model-tiers.js';
12
+
13
+ /**
14
+ * Variant guard suffix table used when the data file omits `variant_suffixes`.
15
+ * Semantics: a model id whose LAST hyphen-separated segment is one of these
16
+ * suffixes is a downgraded variant of its family (glm-5.3-flash, gpt-5.4-mini,
17
+ * …); a family-wide glob catching such a variant must not hand it the
18
+ * flagship tier (see the guard in `matchDataEntry`). Variant-specific
19
+ * selectors (exact ids, provider-scoped ids, or globs ending in the suffix)
20
+ * are deliberate declarations and bypass the guard.
21
+ */
22
+ export const DEFAULT_VARIANT_SUFFIXES: readonly string[] = [
23
+ 'flash', 'highspeed', 'air', 'mini', 'turbo', 'lite', 'nano', 'instant', 'free',
24
+ ];
12
25
 
13
26
  // ── types ────────────────────────────────────────────────────────────
14
27
 
@@ -23,6 +36,8 @@ interface TierDataFile {
23
36
  version: number;
24
37
  updated: string;
25
38
  entries: TierDataEntry[];
39
+ /** Optional. Suffix table governing the glob variant guard; defaults to `DEFAULT_VARIANT_SUFFIXES`. */
40
+ variant_suffixes?: string[];
26
41
  }
27
42
 
28
43
  // ── state ────────────────────────────────────────────────────────────
@@ -79,12 +94,27 @@ export function matchDataEntry(
79
94
  const bare = data.entries.find((e) => e.selector === modelId);
80
95
  if (bare) return { tier: bare.tier, reputation: bare.reputation, reason: `data: ${bare.selector}` };
81
96
 
82
- // 3. Glob patterns (in order)
97
+ // 3. Glob patterns (in order). A family-wide glob catching an un-enumerated
98
+ // downgraded variant (last hyphen segment in the suffix table) is
99
+ // deterministically demoted one tier instead of riding the flagship
100
+ // rank; globs ending in the suffix are variant-specific declarations
101
+ // and pass through unchanged.
102
+ const suffixes = data.variant_suffixes ?? DEFAULT_VARIANT_SUFFIXES;
83
103
  const globs = data.entries.filter((e) => hasGlob(e.selector));
84
104
  for (const g of globs) {
85
- if (globMatch(g.selector, modelId)) {
86
- return { tier: g.tier, reputation: g.reputation, reason: `data: ${g.selector}` };
105
+ if (!globMatch(g.selector, modelId)) continue;
106
+ const lastSegment = (modelId.split('-').pop() ?? '').toLowerCase();
107
+ const isVariant = lastSegment !== '' && suffixes.includes(lastSegment);
108
+ const variantSpecific = g.selector.toLowerCase().endsWith(lastSegment);
109
+ if (isVariant && !variantSpecific) {
110
+ const rank = Math.max(TIER_RANK[g.tier] - 1, TIER_RANK.tiny);
111
+ return {
112
+ tier: rankToTierName(rank),
113
+ reputation: Math.max(0, g.reputation - 20),
114
+ reason: `data: ${g.selector} (variant guard)`,
115
+ };
87
116
  }
117
+ return { tier: g.tier, reputation: g.reputation, reason: `data: ${g.selector}` };
88
118
  }
89
119
 
90
120
  return null;
@@ -111,7 +141,12 @@ function ensureLoaded(): TierDataFile | null {
111
141
  const raw = readFileSync(CACHE_PATH, 'utf-8');
112
142
  const parsed = JSON.parse(raw) as TierDataFile;
113
143
  if (!Array.isArray(parsed.entries)) throw new Error('missing entries array');
114
- _data = parsed;
144
+ // Normalize the optional suffix table: malformed values fall back to
145
+ // the built-in defaults rather than disabling the guard.
146
+ const suffixes = Array.isArray(parsed.variant_suffixes)
147
+ ? parsed.variant_suffixes.filter((s): s is string => typeof s === 'string' && s.length > 0)
148
+ : [...DEFAULT_VARIANT_SUFFIXES];
149
+ _data = { ...parsed, variant_suffixes: suffixes };
115
150
  _loadError = null;
116
151
  } catch (err) {
117
152
  _data = null;
@@ -5,11 +5,11 @@
5
5
  * when the cache is missing or stale (> 7 days).
6
6
  */
7
7
 
8
- import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
8
+ import { writeFileSync, mkdirSync, existsSync, renameSync, unlinkSync } from 'node:fs';
9
9
  import { homedir } from 'node:os';
10
10
  import { join, dirname } from 'node:path';
11
11
  import type { TierDataEntry } from './tiers-data.js';
12
- import { reloadTierData, TIERS_DATA_PATH, tiersDataAge, isTiersDataLoaded } from './tiers-data.js';
12
+ import { DEFAULT_VARIANT_SUFFIXES, reloadTierData, TIERS_DATA_PATH, tiersDataAge, isTiersDataLoaded } from './tiers-data.js';
13
13
 
14
14
  // ── constants ────────────────────────────────────────────────────────
15
15
 
@@ -36,7 +36,6 @@ const SEED_ENTRIES: TierDataEntry[] = [
36
36
  // ── deepseek ───────────────────────────────────
37
37
  { selector: 'deepseek/deepseek-v4-pro', tier: 'top', reputation: 70 },
38
38
  { selector: 'deepseek/deepseek-v4-flash', tier: 'mid', reputation: 45 },
39
- { selector: 'deepseek/deepseek-v4-pro', tier: 'top', reputation: 70 },
40
39
 
41
40
  // ── minimax ────────────────────────────────────
42
41
  { selector: 'MiniMax-M3', tier: 'top', reputation: 60 },
@@ -51,6 +50,10 @@ const SEED_ENTRIES: TierDataEntry[] = [
51
50
  { selector: 'kimi-k*', tier: 'top', reputation: 60 },
52
51
 
53
52
  // ── zhipu / glm ────────────────────────────────
53
+ // Downgraded variants are enumerated explicitly so they take the mid band
54
+ // (bare id beats the `glm-5.*` family glob) instead of the flagship top/68.
55
+ { selector: 'glm-5.3-flash', tier: 'mid', reputation: 45 },
56
+ { selector: 'glm-5.3-highspeed', tier: 'mid', reputation: 45 },
54
57
  { selector: 'glm-5.2', tier: 'top', reputation: 70 },
55
58
  { selector: 'glm-5.*', tier: 'top', reputation: 68 },
56
59
  { selector: 'glm-4.7', tier: 'mid', reputation: 50 },
@@ -117,29 +120,69 @@ export interface UpdateResult {
117
120
  error?: string;
118
121
  }
119
122
 
123
+ /** Payload written to the cache file. */
124
+ interface TiersPayload {
125
+ version: number;
126
+ updated: string;
127
+ variant_suffixes: string[];
128
+ entries: TierDataEntry[];
129
+ }
130
+
131
+ /**
132
+ * Full integrity/format check on the payload BEFORE it may replace the cache.
133
+ * Returns an error message, or null when the payload is safe to write.
134
+ */
135
+ function validatePayload(data: TiersPayload): string | null {
136
+ if (data.version !== 1) return `unsupported version: ${data.version}`;
137
+ if (typeof data.updated !== 'string' || Number.isNaN(Date.parse(data.updated))) return 'missing/invalid updated timestamp';
138
+ if (!Array.isArray(data.variant_suffixes) || data.variant_suffixes.length === 0) return 'missing variant_suffixes';
139
+ if (!Array.isArray(data.entries) || data.entries.length === 0) return 'empty entries array';
140
+ const seen = new Set<string>();
141
+ for (const e of data.entries) {
142
+ if (typeof e.selector !== 'string' || !e.selector.trim()) return `invalid selector: ${JSON.stringify(e)}`;
143
+ if (typeof e.reputation !== 'number' || !Number.isFinite(e.reputation) || e.reputation < 0) return `invalid reputation for ${e.selector}`;
144
+ if (seen.has(e.selector)) return `duplicate selector: ${e.selector}`;
145
+ seen.add(e.selector);
146
+ }
147
+ return null;
148
+ }
149
+
120
150
  /**
121
151
  * Write seed tier data to the cache file.
122
152
  * Creates parent directories as needed.
153
+ * The payload is validated first, and written via a temp file + rename so a
154
+ * failed write never clobbers an existing cache.
123
155
  */
124
156
  export function updateTiers(opts?: { silent?: boolean }): UpdateResult {
125
157
  const path = TIERS_DATA_PATH;
158
+ const tmpPath = `${path}.tmp`;
126
159
  try {
127
- const dir = dirname(path);
128
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
129
-
130
- const data = {
160
+ const data: TiersPayload = {
131
161
  version: 1,
132
162
  updated: new Date().toISOString(),
163
+ variant_suffixes: [...DEFAULT_VARIANT_SUFFIXES],
133
164
  entries: SEED_ENTRIES,
134
165
  };
135
166
 
136
- writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf-8');
167
+ const invalid = validatePayload(data);
168
+ if (invalid) return { ok: false, path, count: 0, error: `refusing to write: ${invalid}` };
169
+
170
+ const dir = dirname(path);
171
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
172
+
173
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
174
+ renameSync(tmpPath, path);
137
175
 
138
176
  // Force runtime to reload on next access
139
177
  reloadTierData();
140
178
 
141
179
  return { ok: true, path, count: SEED_ENTRIES.length };
142
180
  } catch (err) {
181
+ try {
182
+ if (existsSync(tmpPath)) unlinkSync(tmpPath);
183
+ } catch {
184
+ // best-effort cleanup; the original error below matters more
185
+ }
143
186
  const msg = err instanceof Error ? err.message : String(err);
144
187
  return { ok: false, path, count: 0, error: msg };
145
188
  }