@flame0510/project-aether 1.5.2 → 1.6.1
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/app/api/gateway/provider/route.ts +16 -10
- package/app/api/models/details/route.ts +30 -0
- package/app/api/system-health/route.ts +3 -3
- package/app/api/update-check/route.ts +27 -11
- package/app/components/SessionDrawer.tsx +4 -4
- package/app/components/VersionBanner.tsx +101 -56
- package/app/components/ui/Metric.tsx +7 -2
- package/app/gateway/ModelDetailsModal.tsx +322 -0
- package/app/gateway/PageClient.tsx +28 -10
- package/app/globals.css +4 -1
- package/app/lib/model-format.ts +21 -0
- package/docs/FRONTEND-ARCHITECTURE.md +2 -1
- package/docs/dev/API-REFERENCE.md +41 -2
- package/docs/dev/GATEWAY.md +58 -0
- package/lib/memory-context.ts +2 -2
- package/lib/model-catalogue.ts +17 -0
- package/lib/model-details.ts +124 -0
- package/lib/provider-labels.ts +21 -0
- package/model-details.json +16360 -0
- package/model-pricing.json +59 -59
- package/models.config.json +75 -9
- package/package.json +4 -2
- package/scripts/check-language.mjs +76 -0
- package/scripts/lib/model-upstream.mjs +61 -0
- package/scripts/model-info-suggest.mjs +149 -0
- package/scripts/refresh-model-pricing.mjs +175 -16
package/lib/model-catalogue.ts
CHANGED
|
@@ -14,6 +14,20 @@ import { readProviderKeys } from '@/app/api/gateway/provider/keys';
|
|
|
14
14
|
/* Types */
|
|
15
15
|
/* ------------------------------------------------------------------ */
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* The hand-written `info` block of a catalogue entry: what OpenRouter cannot tell us
|
|
19
|
+
* (parameter counts, the vendor's own docs, benchmarks published elsewhere). Every
|
|
20
|
+
* field is optional and every number carries its source — never an estimate.
|
|
21
|
+
*/
|
|
22
|
+
export interface ModelInfo {
|
|
23
|
+
params?: string;
|
|
24
|
+
released?: string;
|
|
25
|
+
docUrl?: string;
|
|
26
|
+
knowledgeCutoff?: string;
|
|
27
|
+
benchmarks?: { name: string; value: number; source: string; asOf?: string }[];
|
|
28
|
+
notes?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
export interface ModelConfigEntry {
|
|
18
32
|
id: string;
|
|
19
33
|
name: string;
|
|
@@ -22,6 +36,8 @@ export interface ModelConfigEntry {
|
|
|
22
36
|
modality?: string;
|
|
23
37
|
/** Upstream has retired this id. It still answers, but is redirected. */
|
|
24
38
|
deprecated?: boolean;
|
|
39
|
+
/** Curated extras for the details modal (lib/model-details.ts). */
|
|
40
|
+
info?: ModelInfo;
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
/* ------------------------------------------------------------------ */
|
|
@@ -86,6 +102,7 @@ function loadBundledModels(): ModelConfigEntry[] {
|
|
|
86
102
|
enabled: Boolean(m.enabled),
|
|
87
103
|
modality: typeof m.modality === 'string' ? m.modality : undefined,
|
|
88
104
|
deprecated: m.deprecated === true ? true : undefined,
|
|
105
|
+
info: m.info && typeof m.info === 'object' ? (m.info as ModelInfo) : undefined,
|
|
89
106
|
}));
|
|
90
107
|
lastGoodBundle = models;
|
|
91
108
|
bundleStatus = 'ok';
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model details for the details modal: the curated catalogue entry
|
|
3
|
+
* (`models.config.json`), its price (`model-pricing.json`) and the generated specs,
|
|
4
|
+
* description and benchmarks (`model-details.json`, written by
|
|
5
|
+
* `scripts/refresh-model-pricing.mjs` from the OpenRouter catalogue).
|
|
6
|
+
*
|
|
7
|
+
* The generated file is optional on purpose: a checkout that never ran the script has
|
|
8
|
+
* fewer fields, never an error. Prices for direct providers come from their own price list
|
|
9
|
+
* (the script is scoped to `openrouter/*`), so the view labels where each number is from.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { loadModelsConfig, type ModelInfo } from '@/lib/model-catalogue';
|
|
14
|
+
import { providerLabel } from '@/lib/provider-labels';
|
|
15
|
+
import { getPricing } from '@/lib/model-pricing';
|
|
16
|
+
|
|
17
|
+
/** What `scripts/refresh-model-pricing.mjs` writes per id. All optional. */
|
|
18
|
+
export interface GeneratedDetails {
|
|
19
|
+
description?: string;
|
|
20
|
+
/** Release/listing date, `YYYY-MM-DD`. */
|
|
21
|
+
created?: string;
|
|
22
|
+
context?: number;
|
|
23
|
+
/** The provider's own context limit, when OpenRouter reports a different one. */
|
|
24
|
+
providerContext?: number;
|
|
25
|
+
maxOutput?: number;
|
|
26
|
+
tokenizer?: string;
|
|
27
|
+
instructType?: string;
|
|
28
|
+
/** Total parameter count and its source (Hugging Face, open weights only). */
|
|
29
|
+
params?: number;
|
|
30
|
+
paramsSource?: string;
|
|
31
|
+
hfArchitecture?: string;
|
|
32
|
+
knowledgeCutoff?: string;
|
|
33
|
+
huggingFaceId?: string;
|
|
34
|
+
canonicalSlug?: string;
|
|
35
|
+
url?: string;
|
|
36
|
+
reasoning?: { mandatory?: boolean; default_enabled?: boolean; supported_efforts?: string[]; default_effort?: string };
|
|
37
|
+
supportedParameters?: string[];
|
|
38
|
+
moderated?: boolean;
|
|
39
|
+
benchmarks?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ModelDetailsView {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
provider: string;
|
|
46
|
+
/** How the UI names that provider ("GLM (Z.AI)", "OpenRouter"). */
|
|
47
|
+
providerLabel: string;
|
|
48
|
+
modality?: string;
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
deprecated: boolean;
|
|
51
|
+
price: { input: number; output: number; source: 'openrouter' | 'vendor' } | null;
|
|
52
|
+
details: GeneratedDetails | null;
|
|
53
|
+
info: ModelInfo | null;
|
|
54
|
+
/** When the generated part was produced. */
|
|
55
|
+
asOf: string | null;
|
|
56
|
+
detailsSource: 'openrouter' | 'none';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface DetailsFile {
|
|
60
|
+
source?: string;
|
|
61
|
+
generatedAt?: string;
|
|
62
|
+
models?: Record<string, GeneratedDetails>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveDetailsPath(): string {
|
|
66
|
+
const cwdPath = path.resolve(process.cwd(), 'model-details.json');
|
|
67
|
+
if (fs.existsSync(cwdPath)) return cwdPath;
|
|
68
|
+
// Fallback: relative to this module, for a global install whose cwd is elsewhere.
|
|
69
|
+
return path.resolve(__dirname, '..', 'model-details.json');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let cache: DetailsFile | null = null;
|
|
73
|
+
let cacheMtime = 0;
|
|
74
|
+
|
|
75
|
+
function loadDetails(): DetailsFile {
|
|
76
|
+
try {
|
|
77
|
+
const stat = fs.statSync(resolveDetailsPath());
|
|
78
|
+
if (cache && stat.mtimeMs <= cacheMtime) return cache;
|
|
79
|
+
cache = JSON.parse(fs.readFileSync(resolveDetailsPath(), 'utf-8')) as DetailsFile;
|
|
80
|
+
cacheMtime = stat.mtimeMs;
|
|
81
|
+
return cache ?? {};
|
|
82
|
+
} catch {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Parameter count per model id, for the list views that show a size next to the price.
|
|
89
|
+
* One cached read of the generated file; a model without a Hugging Face card is simply
|
|
90
|
+
* absent — the row shows nothing rather than a guess.
|
|
91
|
+
*/
|
|
92
|
+
export function getModelParams(): Record<string, number> {
|
|
93
|
+
const out: Record<string, number> = {};
|
|
94
|
+
for (const [id, details] of Object.entries(loadDetails().models ?? {})) {
|
|
95
|
+
if (typeof details.params === 'number' && details.params > 0) out[id] = details.params;
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The merged view for one model id, or null when the catalogue does not carry it. */
|
|
101
|
+
export function getModelDetails(id: string): ModelDetailsView | null {
|
|
102
|
+
const entry = loadModelsConfig().find((m) => m.id === id);
|
|
103
|
+
if (!entry) return null;
|
|
104
|
+
|
|
105
|
+
const file = loadDetails();
|
|
106
|
+
const details = file.models?.[id] ?? null;
|
|
107
|
+
const pricing = getPricing(id);
|
|
108
|
+
const info = entry.info ?? null;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
id: entry.id,
|
|
112
|
+
name: entry.name,
|
|
113
|
+
provider: entry.provider,
|
|
114
|
+
providerLabel: providerLabel(entry.provider),
|
|
115
|
+
modality: entry.modality,
|
|
116
|
+
enabled: entry.enabled,
|
|
117
|
+
deprecated: entry.deprecated === true,
|
|
118
|
+
price: pricing ? { ...pricing, source: id.startsWith('openrouter/') ? 'openrouter' : 'vendor' } : null,
|
|
119
|
+
details,
|
|
120
|
+
info,
|
|
121
|
+
asOf: file.generatedAt ?? null,
|
|
122
|
+
detailsSource: details ? 'openrouter' : 'none',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the UI calls each provider key the model catalogue uses. One source: the Gateway's
|
|
3
|
+
* provider cards and the details modal both read it, so a provider is never named two ways.
|
|
4
|
+
*/
|
|
5
|
+
export const PROVIDER_LABELS: Record<string, string> = {
|
|
6
|
+
google: 'Google Gemini',
|
|
7
|
+
deepseek: 'DeepSeek',
|
|
8
|
+
openrouter: 'OpenRouter',
|
|
9
|
+
openai: 'OpenAI',
|
|
10
|
+
anthropic: 'Anthropic',
|
|
11
|
+
groq: 'Groq',
|
|
12
|
+
kimi: 'Kimi',
|
|
13
|
+
glm: 'GLM (Z.AI)',
|
|
14
|
+
qwen: 'Qwen',
|
|
15
|
+
rev4a: 'REV4A API KEY',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** The label for a provider key, falling back to the key itself. */
|
|
19
|
+
export function providerLabel(key: string): string {
|
|
20
|
+
return PROVIDER_LABELS[key] ?? key;
|
|
21
|
+
}
|