@codelia/model-metadata 0.1.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/dist/index.cjs +290 -0
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +253 -0
- package/package.json +30 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ModelMetadataServiceImpl: () => ModelMetadataServiceImpl
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/sources/modeldev.ts
|
|
38
|
+
var import_promises = require("fs/promises");
|
|
39
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
40
|
+
var import_storage = require("@codelia/storage");
|
|
41
|
+
var import_zod = require("zod");
|
|
42
|
+
var modelResourceUrl = "https://models.dev/api.json";
|
|
43
|
+
var CACHE_FILENAME = "models.dev.json";
|
|
44
|
+
var CACHE_VERSION = 1;
|
|
45
|
+
var DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
46
|
+
var wireModelDevModelSchema = import_zod.z.object({
|
|
47
|
+
id: import_zod.z.string(),
|
|
48
|
+
name: import_zod.z.string(),
|
|
49
|
+
family: import_zod.z.string().optional(),
|
|
50
|
+
attachment: import_zod.z.boolean().optional(),
|
|
51
|
+
reasoning: import_zod.z.boolean().optional(),
|
|
52
|
+
tool_call: import_zod.z.boolean().optional(),
|
|
53
|
+
structured_output: import_zod.z.boolean().optional(),
|
|
54
|
+
temperature: import_zod.z.boolean().optional(),
|
|
55
|
+
knowledge: import_zod.z.string().optional(),
|
|
56
|
+
release_date: import_zod.z.string().optional(),
|
|
57
|
+
last_updated: import_zod.z.string().optional(),
|
|
58
|
+
open_weights: import_zod.z.boolean().optional(),
|
|
59
|
+
cost: import_zod.z.object({
|
|
60
|
+
input: import_zod.z.number().optional(),
|
|
61
|
+
output: import_zod.z.number().optional(),
|
|
62
|
+
reasoning: import_zod.z.number().optional(),
|
|
63
|
+
cache_read: import_zod.z.number().optional(),
|
|
64
|
+
cache_write: import_zod.z.number().optional(),
|
|
65
|
+
input_audio: import_zod.z.number().optional(),
|
|
66
|
+
output_audio: import_zod.z.number().optional()
|
|
67
|
+
}).loose().optional(),
|
|
68
|
+
limit: import_zod.z.object({
|
|
69
|
+
context: import_zod.z.number().optional(),
|
|
70
|
+
input: import_zod.z.number().optional(),
|
|
71
|
+
output: import_zod.z.number().optional()
|
|
72
|
+
}).loose().optional(),
|
|
73
|
+
modalities: import_zod.z.object({
|
|
74
|
+
input: import_zod.z.array(import_zod.z.string()).optional(),
|
|
75
|
+
output: import_zod.z.array(import_zod.z.string()).optional()
|
|
76
|
+
}).loose().optional(),
|
|
77
|
+
interleaved: import_zod.z.union([
|
|
78
|
+
import_zod.z.boolean(),
|
|
79
|
+
import_zod.z.object({
|
|
80
|
+
field: import_zod.z.string().optional()
|
|
81
|
+
}).loose()
|
|
82
|
+
]).optional()
|
|
83
|
+
}).loose();
|
|
84
|
+
var wireModelDevModelsSchema = import_zod.z.union([
|
|
85
|
+
import_zod.z.array(wireModelDevModelSchema),
|
|
86
|
+
import_zod.z.record(import_zod.z.string(), wireModelDevModelSchema)
|
|
87
|
+
]);
|
|
88
|
+
var wireModelDevProviderSchema = import_zod.z.object({
|
|
89
|
+
id: import_zod.z.string().optional(),
|
|
90
|
+
name: import_zod.z.string().optional(),
|
|
91
|
+
api: import_zod.z.string().optional(),
|
|
92
|
+
doc: import_zod.z.string().optional(),
|
|
93
|
+
env: import_zod.z.array(import_zod.z.string()).optional(),
|
|
94
|
+
npm: import_zod.z.string().optional(),
|
|
95
|
+
models: import_zod.z.record(import_zod.z.string(), wireModelDevModelSchema).optional()
|
|
96
|
+
}).loose();
|
|
97
|
+
var wireModelDevProvidersSchema = import_zod.z.record(
|
|
98
|
+
import_zod.z.string(),
|
|
99
|
+
wireModelDevProviderSchema
|
|
100
|
+
);
|
|
101
|
+
var ModelDevSource = class {
|
|
102
|
+
loaded = false;
|
|
103
|
+
models = {};
|
|
104
|
+
cacheTtlMs;
|
|
105
|
+
cachePath;
|
|
106
|
+
constructor(options = {}) {
|
|
107
|
+
this.cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
108
|
+
const storagePaths = options.storagePathService ? options.storagePathService.resolvePaths({
|
|
109
|
+
rootOverride: options.storageRoot
|
|
110
|
+
}) : (0, import_storage.resolveStoragePaths)({ rootOverride: options.storageRoot });
|
|
111
|
+
this.cachePath = options.cachePath ?? import_node_path.default.join(storagePaths.cacheDir, CACHE_FILENAME);
|
|
112
|
+
}
|
|
113
|
+
async loadModelMetadata(options = {}) {
|
|
114
|
+
this.loaded = false;
|
|
115
|
+
this.models = {};
|
|
116
|
+
if (!options.forceRefresh) {
|
|
117
|
+
const cached = await readCacheFile(this.cachePath);
|
|
118
|
+
if (cached && !isCacheExpired(cached.expiresAt)) {
|
|
119
|
+
this.models = cached.entries;
|
|
120
|
+
this.loaded = true;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const response = await fetch(modelResourceUrl);
|
|
125
|
+
const data = await response.json();
|
|
126
|
+
const providersResult = wireModelDevProvidersSchema.safeParse(data);
|
|
127
|
+
if (providersResult.success) {
|
|
128
|
+
this.models = buildEntriesFromProviders(providersResult.data);
|
|
129
|
+
}
|
|
130
|
+
let hasEntries = Object.keys(this.models).length > 0;
|
|
131
|
+
if (!hasEntries) {
|
|
132
|
+
this.models = buildEntriesFromLegacyPayload(data);
|
|
133
|
+
}
|
|
134
|
+
hasEntries = Object.keys(this.models).length > 0;
|
|
135
|
+
if (!hasEntries) {
|
|
136
|
+
throw new Error("ModelDevSource: empty metadata payload");
|
|
137
|
+
}
|
|
138
|
+
await writeCacheFile(this.cachePath, {
|
|
139
|
+
version: CACHE_VERSION,
|
|
140
|
+
cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
141
|
+
expiresAt: new Date(Date.now() + this.cacheTtlMs).toISOString(),
|
|
142
|
+
entries: this.models
|
|
143
|
+
}).catch(() => void 0);
|
|
144
|
+
this.loaded = true;
|
|
145
|
+
}
|
|
146
|
+
async getModelEntry(provider, model) {
|
|
147
|
+
if (!this.loaded) {
|
|
148
|
+
await this.loadModelMetadata();
|
|
149
|
+
}
|
|
150
|
+
return this.models[provider]?.[model] || null;
|
|
151
|
+
}
|
|
152
|
+
async getModelEntries(provider) {
|
|
153
|
+
if (!this.loaded) {
|
|
154
|
+
await this.loadModelMetadata();
|
|
155
|
+
}
|
|
156
|
+
return this.models[provider] ? Object.values(this.models[provider]) : null;
|
|
157
|
+
}
|
|
158
|
+
async getAllModelEntries() {
|
|
159
|
+
if (!this.loaded) {
|
|
160
|
+
await this.loadModelMetadata();
|
|
161
|
+
}
|
|
162
|
+
return this.models;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
function buildEntriesFromProviders(providers) {
|
|
166
|
+
const entries = {};
|
|
167
|
+
for (const [providerId, provider] of Object.entries(providers)) {
|
|
168
|
+
const models = provider.models ? Object.values(provider.models) : [];
|
|
169
|
+
if (models.length === 0) continue;
|
|
170
|
+
entries[providerId] = buildEntriesForProvider(providerId, models);
|
|
171
|
+
}
|
|
172
|
+
return entries;
|
|
173
|
+
}
|
|
174
|
+
function buildEntriesFromLegacyPayload(data) {
|
|
175
|
+
let provider = "unknown";
|
|
176
|
+
if (data.provider && typeof data.provider === "object") {
|
|
177
|
+
const name = data.provider.name;
|
|
178
|
+
if (name) provider = name;
|
|
179
|
+
}
|
|
180
|
+
const modelsResult = wireModelDevModelsSchema.safeParse(
|
|
181
|
+
data.models
|
|
182
|
+
);
|
|
183
|
+
if (!modelsResult.success) return {};
|
|
184
|
+
const models = Array.isArray(modelsResult.data) ? modelsResult.data : Object.values(modelsResult.data);
|
|
185
|
+
if (models.length === 0) return {};
|
|
186
|
+
return { [provider]: buildEntriesForProvider(provider, models) };
|
|
187
|
+
}
|
|
188
|
+
function buildEntriesForProvider(provider, models) {
|
|
189
|
+
const entries = {};
|
|
190
|
+
for (const model of models) {
|
|
191
|
+
entries[model.id] = {
|
|
192
|
+
provider,
|
|
193
|
+
modelId: model.id,
|
|
194
|
+
cost: {
|
|
195
|
+
input: model.cost?.input,
|
|
196
|
+
output: model.cost?.output,
|
|
197
|
+
reasoning: model.cost?.reasoning,
|
|
198
|
+
cacheRead: model.cost?.cache_read,
|
|
199
|
+
cacheWrite: model.cost?.cache_write,
|
|
200
|
+
inputAudio: model.cost?.input_audio,
|
|
201
|
+
outputAudio: model.cost?.output_audio
|
|
202
|
+
},
|
|
203
|
+
limits: {
|
|
204
|
+
contextWindow: model.limit?.context,
|
|
205
|
+
inputTokens: model.limit?.input,
|
|
206
|
+
outputTokens: model.limit?.output
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return entries;
|
|
211
|
+
}
|
|
212
|
+
async function readCacheFile(cachePath) {
|
|
213
|
+
const contents = await (0, import_promises.readFile)(cachePath, "utf8").catch(() => null);
|
|
214
|
+
if (!contents) return null;
|
|
215
|
+
const parsed = safeJsonParse(contents);
|
|
216
|
+
if (!parsed || !isCacheFile(parsed)) return null;
|
|
217
|
+
return parsed;
|
|
218
|
+
}
|
|
219
|
+
async function writeCacheFile(cachePath, cache) {
|
|
220
|
+
await ensureDir(import_node_path.default.dirname(cachePath));
|
|
221
|
+
await (0, import_promises.writeFile)(cachePath, JSON.stringify(cache), "utf8");
|
|
222
|
+
}
|
|
223
|
+
function isCacheExpired(expiresAt) {
|
|
224
|
+
const timestamp = Date.parse(expiresAt);
|
|
225
|
+
if (Number.isNaN(timestamp)) return true;
|
|
226
|
+
return timestamp <= Date.now();
|
|
227
|
+
}
|
|
228
|
+
function isCacheFile(value) {
|
|
229
|
+
if (!value || typeof value !== "object") return false;
|
|
230
|
+
const file = value;
|
|
231
|
+
if (file.version !== CACHE_VERSION) return false;
|
|
232
|
+
if (typeof file.cachedAt !== "string") return false;
|
|
233
|
+
if (typeof file.expiresAt !== "string") return false;
|
|
234
|
+
if (!file.entries || typeof file.entries !== "object") return false;
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
function safeJsonParse(value) {
|
|
238
|
+
try {
|
|
239
|
+
return JSON.parse(value);
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function ensureDir(dir) {
|
|
245
|
+
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/service.ts
|
|
249
|
+
var ModelMetadataServiceImpl = class {
|
|
250
|
+
modelDevSource;
|
|
251
|
+
sources;
|
|
252
|
+
constructor(options = {}) {
|
|
253
|
+
this.modelDevSource = new ModelDevSource(options);
|
|
254
|
+
this.sources = [this.modelDevSource];
|
|
255
|
+
}
|
|
256
|
+
async refreshAllModelEntries() {
|
|
257
|
+
await this.modelDevSource.loadModelMetadata({ forceRefresh: true });
|
|
258
|
+
return this.getAllModelEntries();
|
|
259
|
+
}
|
|
260
|
+
async getModelEntry(provider, model) {
|
|
261
|
+
for (const source of this.sources) {
|
|
262
|
+
const entry = await source.getModelEntry(provider, model);
|
|
263
|
+
if (entry) {
|
|
264
|
+
return entry;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
async getModelEntries(provider) {
|
|
270
|
+
for (const source of this.sources) {
|
|
271
|
+
const entries = await source.getModelEntries(provider);
|
|
272
|
+
if (entries) {
|
|
273
|
+
return entries;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
async getAllModelEntries() {
|
|
279
|
+
const entries = {};
|
|
280
|
+
for (const source of this.sources) {
|
|
281
|
+
const sourceEntries = await source.getAllModelEntries();
|
|
282
|
+
Object.assign(entries, sourceEntries);
|
|
283
|
+
}
|
|
284
|
+
return entries;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
288
|
+
0 && (module.exports = {
|
|
289
|
+
ModelMetadataServiceImpl
|
|
290
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ModelEntry, StoragePathService, ModelMetadataService } from '@codelia/core';
|
|
2
|
+
export { ModelCost, ModelEntry, ModelLimits, ModelMetadataIndex } from '@codelia/core';
|
|
3
|
+
|
|
4
|
+
type ModelMetadataSource = {
|
|
5
|
+
getModelEntry(provider: string, modelId: string): Promise<ModelEntry | null>;
|
|
6
|
+
getModelEntries(provider: string): Promise<ModelEntry[] | null>;
|
|
7
|
+
getAllModelEntries(): Promise<Record<string, Record<string, ModelEntry>>>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type ModelDevSourceOptions = {
|
|
11
|
+
cacheTtlMs?: number;
|
|
12
|
+
cachePath?: string;
|
|
13
|
+
storageRoot?: string;
|
|
14
|
+
storagePathService?: StoragePathService;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
declare class ModelMetadataServiceImpl implements ModelMetadataService {
|
|
18
|
+
private readonly modelDevSource;
|
|
19
|
+
private readonly sources;
|
|
20
|
+
constructor(options?: ModelDevSourceOptions);
|
|
21
|
+
refreshAllModelEntries(): Promise<Record<string, Record<string, ModelEntry>>>;
|
|
22
|
+
getModelEntry(provider: string, model: string): Promise<ModelEntry | null>;
|
|
23
|
+
getModelEntries(provider: string): Promise<ModelEntry[] | null>;
|
|
24
|
+
getAllModelEntries(): Promise<Record<string, Record<string, ModelEntry>>>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { type ModelDevSourceOptions, ModelMetadataServiceImpl, type ModelMetadataSource };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ModelEntry, StoragePathService, ModelMetadataService } from '@codelia/core';
|
|
2
|
+
export { ModelCost, ModelEntry, ModelLimits, ModelMetadataIndex } from '@codelia/core';
|
|
3
|
+
|
|
4
|
+
type ModelMetadataSource = {
|
|
5
|
+
getModelEntry(provider: string, modelId: string): Promise<ModelEntry | null>;
|
|
6
|
+
getModelEntries(provider: string): Promise<ModelEntry[] | null>;
|
|
7
|
+
getAllModelEntries(): Promise<Record<string, Record<string, ModelEntry>>>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type ModelDevSourceOptions = {
|
|
11
|
+
cacheTtlMs?: number;
|
|
12
|
+
cachePath?: string;
|
|
13
|
+
storageRoot?: string;
|
|
14
|
+
storagePathService?: StoragePathService;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
declare class ModelMetadataServiceImpl implements ModelMetadataService {
|
|
18
|
+
private readonly modelDevSource;
|
|
19
|
+
private readonly sources;
|
|
20
|
+
constructor(options?: ModelDevSourceOptions);
|
|
21
|
+
refreshAllModelEntries(): Promise<Record<string, Record<string, ModelEntry>>>;
|
|
22
|
+
getModelEntry(provider: string, model: string): Promise<ModelEntry | null>;
|
|
23
|
+
getModelEntries(provider: string): Promise<ModelEntry[] | null>;
|
|
24
|
+
getAllModelEntries(): Promise<Record<string, Record<string, ModelEntry>>>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { type ModelDevSourceOptions, ModelMetadataServiceImpl, type ModelMetadataSource };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// src/sources/modeldev.ts
|
|
2
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { resolveStoragePaths } from "@codelia/storage";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
var modelResourceUrl = "https://models.dev/api.json";
|
|
7
|
+
var CACHE_FILENAME = "models.dev.json";
|
|
8
|
+
var CACHE_VERSION = 1;
|
|
9
|
+
var DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
10
|
+
var wireModelDevModelSchema = z.object({
|
|
11
|
+
id: z.string(),
|
|
12
|
+
name: z.string(),
|
|
13
|
+
family: z.string().optional(),
|
|
14
|
+
attachment: z.boolean().optional(),
|
|
15
|
+
reasoning: z.boolean().optional(),
|
|
16
|
+
tool_call: z.boolean().optional(),
|
|
17
|
+
structured_output: z.boolean().optional(),
|
|
18
|
+
temperature: z.boolean().optional(),
|
|
19
|
+
knowledge: z.string().optional(),
|
|
20
|
+
release_date: z.string().optional(),
|
|
21
|
+
last_updated: z.string().optional(),
|
|
22
|
+
open_weights: z.boolean().optional(),
|
|
23
|
+
cost: z.object({
|
|
24
|
+
input: z.number().optional(),
|
|
25
|
+
output: z.number().optional(),
|
|
26
|
+
reasoning: z.number().optional(),
|
|
27
|
+
cache_read: z.number().optional(),
|
|
28
|
+
cache_write: z.number().optional(),
|
|
29
|
+
input_audio: z.number().optional(),
|
|
30
|
+
output_audio: z.number().optional()
|
|
31
|
+
}).loose().optional(),
|
|
32
|
+
limit: z.object({
|
|
33
|
+
context: z.number().optional(),
|
|
34
|
+
input: z.number().optional(),
|
|
35
|
+
output: z.number().optional()
|
|
36
|
+
}).loose().optional(),
|
|
37
|
+
modalities: z.object({
|
|
38
|
+
input: z.array(z.string()).optional(),
|
|
39
|
+
output: z.array(z.string()).optional()
|
|
40
|
+
}).loose().optional(),
|
|
41
|
+
interleaved: z.union([
|
|
42
|
+
z.boolean(),
|
|
43
|
+
z.object({
|
|
44
|
+
field: z.string().optional()
|
|
45
|
+
}).loose()
|
|
46
|
+
]).optional()
|
|
47
|
+
}).loose();
|
|
48
|
+
var wireModelDevModelsSchema = z.union([
|
|
49
|
+
z.array(wireModelDevModelSchema),
|
|
50
|
+
z.record(z.string(), wireModelDevModelSchema)
|
|
51
|
+
]);
|
|
52
|
+
var wireModelDevProviderSchema = z.object({
|
|
53
|
+
id: z.string().optional(),
|
|
54
|
+
name: z.string().optional(),
|
|
55
|
+
api: z.string().optional(),
|
|
56
|
+
doc: z.string().optional(),
|
|
57
|
+
env: z.array(z.string()).optional(),
|
|
58
|
+
npm: z.string().optional(),
|
|
59
|
+
models: z.record(z.string(), wireModelDevModelSchema).optional()
|
|
60
|
+
}).loose();
|
|
61
|
+
var wireModelDevProvidersSchema = z.record(
|
|
62
|
+
z.string(),
|
|
63
|
+
wireModelDevProviderSchema
|
|
64
|
+
);
|
|
65
|
+
var ModelDevSource = class {
|
|
66
|
+
loaded = false;
|
|
67
|
+
models = {};
|
|
68
|
+
cacheTtlMs;
|
|
69
|
+
cachePath;
|
|
70
|
+
constructor(options = {}) {
|
|
71
|
+
this.cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
72
|
+
const storagePaths = options.storagePathService ? options.storagePathService.resolvePaths({
|
|
73
|
+
rootOverride: options.storageRoot
|
|
74
|
+
}) : resolveStoragePaths({ rootOverride: options.storageRoot });
|
|
75
|
+
this.cachePath = options.cachePath ?? path.join(storagePaths.cacheDir, CACHE_FILENAME);
|
|
76
|
+
}
|
|
77
|
+
async loadModelMetadata(options = {}) {
|
|
78
|
+
this.loaded = false;
|
|
79
|
+
this.models = {};
|
|
80
|
+
if (!options.forceRefresh) {
|
|
81
|
+
const cached = await readCacheFile(this.cachePath);
|
|
82
|
+
if (cached && !isCacheExpired(cached.expiresAt)) {
|
|
83
|
+
this.models = cached.entries;
|
|
84
|
+
this.loaded = true;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const response = await fetch(modelResourceUrl);
|
|
89
|
+
const data = await response.json();
|
|
90
|
+
const providersResult = wireModelDevProvidersSchema.safeParse(data);
|
|
91
|
+
if (providersResult.success) {
|
|
92
|
+
this.models = buildEntriesFromProviders(providersResult.data);
|
|
93
|
+
}
|
|
94
|
+
let hasEntries = Object.keys(this.models).length > 0;
|
|
95
|
+
if (!hasEntries) {
|
|
96
|
+
this.models = buildEntriesFromLegacyPayload(data);
|
|
97
|
+
}
|
|
98
|
+
hasEntries = Object.keys(this.models).length > 0;
|
|
99
|
+
if (!hasEntries) {
|
|
100
|
+
throw new Error("ModelDevSource: empty metadata payload");
|
|
101
|
+
}
|
|
102
|
+
await writeCacheFile(this.cachePath, {
|
|
103
|
+
version: CACHE_VERSION,
|
|
104
|
+
cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
105
|
+
expiresAt: new Date(Date.now() + this.cacheTtlMs).toISOString(),
|
|
106
|
+
entries: this.models
|
|
107
|
+
}).catch(() => void 0);
|
|
108
|
+
this.loaded = true;
|
|
109
|
+
}
|
|
110
|
+
async getModelEntry(provider, model) {
|
|
111
|
+
if (!this.loaded) {
|
|
112
|
+
await this.loadModelMetadata();
|
|
113
|
+
}
|
|
114
|
+
return this.models[provider]?.[model] || null;
|
|
115
|
+
}
|
|
116
|
+
async getModelEntries(provider) {
|
|
117
|
+
if (!this.loaded) {
|
|
118
|
+
await this.loadModelMetadata();
|
|
119
|
+
}
|
|
120
|
+
return this.models[provider] ? Object.values(this.models[provider]) : null;
|
|
121
|
+
}
|
|
122
|
+
async getAllModelEntries() {
|
|
123
|
+
if (!this.loaded) {
|
|
124
|
+
await this.loadModelMetadata();
|
|
125
|
+
}
|
|
126
|
+
return this.models;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
function buildEntriesFromProviders(providers) {
|
|
130
|
+
const entries = {};
|
|
131
|
+
for (const [providerId, provider] of Object.entries(providers)) {
|
|
132
|
+
const models = provider.models ? Object.values(provider.models) : [];
|
|
133
|
+
if (models.length === 0) continue;
|
|
134
|
+
entries[providerId] = buildEntriesForProvider(providerId, models);
|
|
135
|
+
}
|
|
136
|
+
return entries;
|
|
137
|
+
}
|
|
138
|
+
function buildEntriesFromLegacyPayload(data) {
|
|
139
|
+
let provider = "unknown";
|
|
140
|
+
if (data.provider && typeof data.provider === "object") {
|
|
141
|
+
const name = data.provider.name;
|
|
142
|
+
if (name) provider = name;
|
|
143
|
+
}
|
|
144
|
+
const modelsResult = wireModelDevModelsSchema.safeParse(
|
|
145
|
+
data.models
|
|
146
|
+
);
|
|
147
|
+
if (!modelsResult.success) return {};
|
|
148
|
+
const models = Array.isArray(modelsResult.data) ? modelsResult.data : Object.values(modelsResult.data);
|
|
149
|
+
if (models.length === 0) return {};
|
|
150
|
+
return { [provider]: buildEntriesForProvider(provider, models) };
|
|
151
|
+
}
|
|
152
|
+
function buildEntriesForProvider(provider, models) {
|
|
153
|
+
const entries = {};
|
|
154
|
+
for (const model of models) {
|
|
155
|
+
entries[model.id] = {
|
|
156
|
+
provider,
|
|
157
|
+
modelId: model.id,
|
|
158
|
+
cost: {
|
|
159
|
+
input: model.cost?.input,
|
|
160
|
+
output: model.cost?.output,
|
|
161
|
+
reasoning: model.cost?.reasoning,
|
|
162
|
+
cacheRead: model.cost?.cache_read,
|
|
163
|
+
cacheWrite: model.cost?.cache_write,
|
|
164
|
+
inputAudio: model.cost?.input_audio,
|
|
165
|
+
outputAudio: model.cost?.output_audio
|
|
166
|
+
},
|
|
167
|
+
limits: {
|
|
168
|
+
contextWindow: model.limit?.context,
|
|
169
|
+
inputTokens: model.limit?.input,
|
|
170
|
+
outputTokens: model.limit?.output
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return entries;
|
|
175
|
+
}
|
|
176
|
+
async function readCacheFile(cachePath) {
|
|
177
|
+
const contents = await readFile(cachePath, "utf8").catch(() => null);
|
|
178
|
+
if (!contents) return null;
|
|
179
|
+
const parsed = safeJsonParse(contents);
|
|
180
|
+
if (!parsed || !isCacheFile(parsed)) return null;
|
|
181
|
+
return parsed;
|
|
182
|
+
}
|
|
183
|
+
async function writeCacheFile(cachePath, cache) {
|
|
184
|
+
await ensureDir(path.dirname(cachePath));
|
|
185
|
+
await writeFile(cachePath, JSON.stringify(cache), "utf8");
|
|
186
|
+
}
|
|
187
|
+
function isCacheExpired(expiresAt) {
|
|
188
|
+
const timestamp = Date.parse(expiresAt);
|
|
189
|
+
if (Number.isNaN(timestamp)) return true;
|
|
190
|
+
return timestamp <= Date.now();
|
|
191
|
+
}
|
|
192
|
+
function isCacheFile(value) {
|
|
193
|
+
if (!value || typeof value !== "object") return false;
|
|
194
|
+
const file = value;
|
|
195
|
+
if (file.version !== CACHE_VERSION) return false;
|
|
196
|
+
if (typeof file.cachedAt !== "string") return false;
|
|
197
|
+
if (typeof file.expiresAt !== "string") return false;
|
|
198
|
+
if (!file.entries || typeof file.entries !== "object") return false;
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
function safeJsonParse(value) {
|
|
202
|
+
try {
|
|
203
|
+
return JSON.parse(value);
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function ensureDir(dir) {
|
|
209
|
+
await mkdir(dir, { recursive: true });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/service.ts
|
|
213
|
+
var ModelMetadataServiceImpl = class {
|
|
214
|
+
modelDevSource;
|
|
215
|
+
sources;
|
|
216
|
+
constructor(options = {}) {
|
|
217
|
+
this.modelDevSource = new ModelDevSource(options);
|
|
218
|
+
this.sources = [this.modelDevSource];
|
|
219
|
+
}
|
|
220
|
+
async refreshAllModelEntries() {
|
|
221
|
+
await this.modelDevSource.loadModelMetadata({ forceRefresh: true });
|
|
222
|
+
return this.getAllModelEntries();
|
|
223
|
+
}
|
|
224
|
+
async getModelEntry(provider, model) {
|
|
225
|
+
for (const source of this.sources) {
|
|
226
|
+
const entry = await source.getModelEntry(provider, model);
|
|
227
|
+
if (entry) {
|
|
228
|
+
return entry;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
async getModelEntries(provider) {
|
|
234
|
+
for (const source of this.sources) {
|
|
235
|
+
const entries = await source.getModelEntries(provider);
|
|
236
|
+
if (entries) {
|
|
237
|
+
return entries;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
async getAllModelEntries() {
|
|
243
|
+
const entries = {};
|
|
244
|
+
for (const source of this.sources) {
|
|
245
|
+
const sourceEntries = await source.getAllModelEntries();
|
|
246
|
+
Object.assign(entries, sourceEntries);
|
|
247
|
+
}
|
|
248
|
+
return entries;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
export {
|
|
252
|
+
ModelMetadataServiceImpl
|
|
253
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codelia/model-metadata",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"require": "./dist/index.cjs"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
20
|
+
"typecheck": "tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@codelia/core": "0.1.0",
|
|
24
|
+
"@codelia/storage": "0.1.0",
|
|
25
|
+
"zod": "^4.3.5"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
}
|
|
30
|
+
}
|