@oh-my-pi/pi-ai 12.9.0 → 12.10.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/package.json +11 -10
- package/src/index.ts +3 -0
- package/src/model-manager.ts +440 -0
- package/src/models.json +656 -46
- package/src/models.ts +12 -6
- package/src/provider-models/google.ts +90 -0
- package/src/provider-models/index.ts +3 -0
- package/src/provider-models/openai-compat.ts +739 -0
- package/src/provider-models/special.ts +106 -0
- package/src/providers/anthropic.ts +20 -6
- package/src/stream.ts +11 -3
- package/src/utils/discovery/antigravity.ts +266 -0
- package/src/utils/discovery/codex.ts +373 -0
- package/src/utils/discovery/cursor.ts +270 -0
- package/src/utils/discovery/gemini.ts +249 -0
- package/src/utils/discovery/index.ts +5 -0
- package/src/utils/discovery/openai-compatible.ts +225 -0
- package/src/utils/oauth/github-copilot.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.10.0",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"@connectrpc/connect-node": "^2.1.1",
|
|
64
64
|
"@google/genai": "^1.41.0",
|
|
65
65
|
"@mistralai/mistralai": "^1.14.0",
|
|
66
|
-
"@oh-my-pi/pi-utils": "12.
|
|
66
|
+
"@oh-my-pi/pi-utils": "12.10.0",
|
|
67
67
|
"@sinclair/typebox": "^0.34.48",
|
|
68
68
|
"@smithy/node-http-handler": "^4.4.10",
|
|
69
69
|
"ajv": "^8.18.0",
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
"chalk": "^5.6.2",
|
|
72
72
|
"openai": "^6.22.0",
|
|
73
73
|
"partial-json": "^0.1.7",
|
|
74
|
+
"zod": "^4.3.6",
|
|
74
75
|
"zod-to-json-schema": "^3.25.1"
|
|
75
76
|
},
|
|
76
77
|
"keywords": [
|
|
@@ -92,12 +93,12 @@
|
|
|
92
93
|
},
|
|
93
94
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
94
95
|
"bugs": {
|
|
95
|
-
|
|
96
|
-
},
|
|
97
|
-
"engines": {
|
|
98
|
-
|
|
99
|
-
},
|
|
100
|
-
"devDependencies": {
|
|
101
|
-
|
|
102
|
-
}
|
|
96
|
+
"url": "https://github.com/can1357/oh-my-pi/issues"
|
|
97
|
+
},
|
|
98
|
+
"engines": {
|
|
99
|
+
"bun": ">=1.3.7"
|
|
100
|
+
},
|
|
101
|
+
"devDependencies": {
|
|
102
|
+
"@types/bun": "^1.3.9"
|
|
103
|
+
}
|
|
103
104
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export type { Static, TSchema } from "@sinclair/typebox";
|
|
2
2
|
export { Type } from "@sinclair/typebox";
|
|
3
3
|
export * from "./api-registry";
|
|
4
|
+
export * from "./model-manager";
|
|
4
5
|
export * from "./models";
|
|
5
6
|
export * from "./provider-details";
|
|
7
|
+
export * from "./provider-models";
|
|
6
8
|
export * from "./providers/anthropic";
|
|
7
9
|
export * from "./providers/azure-openai-responses";
|
|
8
10
|
export * from "./providers/cursor";
|
|
@@ -23,6 +25,7 @@ export * from "./usage/kimi";
|
|
|
23
25
|
export * from "./usage/minimax-code";
|
|
24
26
|
export * from "./usage/openai-codex";
|
|
25
27
|
export * from "./usage/zai";
|
|
28
|
+
export * from "./utils/discovery";
|
|
26
29
|
export * from "./utils/event-stream";
|
|
27
30
|
export * from "./utils/oauth";
|
|
28
31
|
export * from "./utils/overflow";
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
|
|
5
|
+
import { type GeneratedProvider, getBundledModels } from "./models";
|
|
6
|
+
import type { Api, Model, Provider } from "./types";
|
|
7
|
+
|
|
8
|
+
const CACHE_SCHEMA_VERSION = 1;
|
|
9
|
+
const DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Controls when dynamic endpoint models should be fetched.
|
|
13
|
+
*/
|
|
14
|
+
export type ModelRefreshStrategy = "online" | "offline" | "online-if-uncached";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Hook for loading and mapping models.dev fallback data into canonical model objects.
|
|
18
|
+
*/
|
|
19
|
+
export interface ModelsDevFallback<TApi extends Api = Api, TPayload = unknown> {
|
|
20
|
+
/** Fetches raw fallback payload (for example from models.dev). */
|
|
21
|
+
fetch(): Promise<TPayload>;
|
|
22
|
+
/** Maps payload into provider models. */
|
|
23
|
+
map(payload: TPayload, providerId: Provider): readonly Model<TApi>[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configuration for provider model resolution.
|
|
28
|
+
*/
|
|
29
|
+
export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload = unknown> {
|
|
30
|
+
/** Provider id used for static lookup and cache namespacing. */
|
|
31
|
+
providerId: Provider;
|
|
32
|
+
/** Optional static list override. When omitted, bundled models.json is used. */
|
|
33
|
+
staticModels?: readonly Model<TApi>[];
|
|
34
|
+
/** Optional absolute cache path override. Default: <agent-dir>/models/<provider>.json. */
|
|
35
|
+
cachePath?: string;
|
|
36
|
+
/** Maximum cache age in milliseconds before considered stale. Default: 24h. */
|
|
37
|
+
cacheTtlMs?: number;
|
|
38
|
+
/** Optional dynamic endpoint fetcher. */
|
|
39
|
+
fetchDynamicModels?: () => Promise<readonly Model<TApi>[] | null>;
|
|
40
|
+
/** Optional models.dev fallback hook. */
|
|
41
|
+
modelsDev?: ModelsDevFallback<TApi, TModelsDevPayload>;
|
|
42
|
+
/** Clock override for deterministic tests. */
|
|
43
|
+
now?: () => number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolution result.
|
|
48
|
+
*
|
|
49
|
+
* `stale` is false when the resolved catalog is authoritative for the selected provider:
|
|
50
|
+
* - dynamic endpoint data was fetched in this call,
|
|
51
|
+
* - a still-fresh authoritative cache was reused in `online-if-uncached` mode, or
|
|
52
|
+
* - the provider has no dynamic fetcher configured.
|
|
53
|
+
*/
|
|
54
|
+
export interface ModelResolutionResult<TApi extends Api = Api> {
|
|
55
|
+
models: Model<TApi>[];
|
|
56
|
+
stale: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Stateful facade over provider model resolution.
|
|
61
|
+
*/
|
|
62
|
+
export interface ModelManager<TApi extends Api = Api> {
|
|
63
|
+
refresh(strategy?: ModelRefreshStrategy): Promise<ModelResolutionResult<TApi>>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface CachedProviderModels<TApi extends Api = Api> {
|
|
67
|
+
version: number;
|
|
68
|
+
providerId: string;
|
|
69
|
+
updatedAt: number;
|
|
70
|
+
models: Model<TApi>[];
|
|
71
|
+
authoritative: boolean;
|
|
72
|
+
}
|
|
73
|
+
interface CacheReadResult<TApi extends Api = Api> {
|
|
74
|
+
models: Model<TApi>[];
|
|
75
|
+
fresh: boolean;
|
|
76
|
+
authoritative: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates a reusable provider model manager.
|
|
81
|
+
*/
|
|
82
|
+
export function createModelManager<TApi extends Api = Api, TModelsDevPayload = unknown>(
|
|
83
|
+
options: ModelManagerOptions<TApi, TModelsDevPayload>,
|
|
84
|
+
): ModelManager<TApi> {
|
|
85
|
+
return {
|
|
86
|
+
refresh(strategy: ModelRefreshStrategy = "online-if-uncached") {
|
|
87
|
+
return resolveProviderModels(options, strategy);
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolves provider models with source precedence:
|
|
94
|
+
* static -> models.dev -> cache -> dynamic.
|
|
95
|
+
*
|
|
96
|
+
* Later sources override earlier ones by model id.
|
|
97
|
+
*/
|
|
98
|
+
export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPayload = unknown>(
|
|
99
|
+
options: ModelManagerOptions<TApi, TModelsDevPayload>,
|
|
100
|
+
strategy: ModelRefreshStrategy = "online-if-uncached",
|
|
101
|
+
): Promise<ModelResolutionResult<TApi>> {
|
|
102
|
+
const now = options.now ?? Date.now;
|
|
103
|
+
const ttlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
104
|
+
const cachePath = options.cachePath ?? getDefaultCachePath(options.providerId);
|
|
105
|
+
const staticModels = normalizeModelList<TApi>(
|
|
106
|
+
options.staticModels ?? getBundledModels(options.providerId as GeneratedProvider),
|
|
107
|
+
);
|
|
108
|
+
const cache = await readCache<TApi>(cachePath, options.providerId, ttlMs, now);
|
|
109
|
+
const dynamicFetcher = options.fetchDynamicModels;
|
|
110
|
+
const hasDynamicFetcher = typeof dynamicFetcher === "function";
|
|
111
|
+
const hasAuthoritativeCache = (cache?.authoritative ?? false) || !hasDynamicFetcher;
|
|
112
|
+
const shouldFetchFromNetwork = shouldFetchRemoteSources(strategy, cache?.fresh ?? false, hasAuthoritativeCache);
|
|
113
|
+
const fetchedModelsDevModels = shouldFetchFromNetwork ? await fetchModelsDev(options) : null;
|
|
114
|
+
const modelsDevModels = normalizeModelList<TApi>(fetchedModelsDevModels ?? []);
|
|
115
|
+
const shouldUseFreshCacheAsAuthoritative =
|
|
116
|
+
strategy === "online-if-uncached" && (cache?.fresh ?? false) && hasAuthoritativeCache;
|
|
117
|
+
let fetchedDynamicModels: Model<TApi>[] | null = null;
|
|
118
|
+
if (dynamicFetcher && shouldFetchFromNetwork) {
|
|
119
|
+
fetchedDynamicModels = await fetchDynamicModels(dynamicFetcher);
|
|
120
|
+
}
|
|
121
|
+
const dynamicFetchSucceeded = fetchedDynamicModels !== null;
|
|
122
|
+
const cacheModels = dynamicFetchSucceeded ? [] : (cache?.models ?? []);
|
|
123
|
+
const dynamicModels = fetchedDynamicModels ?? [];
|
|
124
|
+
const mergedWithoutDynamic = mergeModelSources(staticModels, modelsDevModels, cacheModels);
|
|
125
|
+
const models = mergeDynamicModels(mergedWithoutDynamic, dynamicModels);
|
|
126
|
+
const dynamicAuthoritative = !hasDynamicFetcher || dynamicFetchSucceeded || shouldUseFreshCacheAsAuthoritative;
|
|
127
|
+
if (shouldFetchFromNetwork) {
|
|
128
|
+
if (dynamicFetchSucceeded) {
|
|
129
|
+
const snapshotModels = mergeDynamicModels(mergeModelSources(staticModels, modelsDevModels), dynamicModels);
|
|
130
|
+
await writeCache(cachePath, {
|
|
131
|
+
version: CACHE_SCHEMA_VERSION,
|
|
132
|
+
providerId: options.providerId,
|
|
133
|
+
updatedAt: now(),
|
|
134
|
+
models: snapshotModels,
|
|
135
|
+
authoritative: true,
|
|
136
|
+
});
|
|
137
|
+
} else if (fetchedModelsDevModels !== null) {
|
|
138
|
+
const latestCache = await readCache<TApi>(cachePath, options.providerId, ttlMs, now);
|
|
139
|
+
if (!latestCache?.authoritative) {
|
|
140
|
+
await writeCache(cachePath, {
|
|
141
|
+
version: CACHE_SCHEMA_VERSION,
|
|
142
|
+
providerId: options.providerId,
|
|
143
|
+
updatedAt: now(),
|
|
144
|
+
models: mergeModelSources(staticModels, modelsDevModels),
|
|
145
|
+
authoritative: false,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
models,
|
|
152
|
+
stale: !dynamicAuthoritative,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getDefaultCachePath(providerId: string): string {
|
|
157
|
+
const encodedProvider = encodeURIComponent(providerId);
|
|
158
|
+
return path.join(getAgentDir(), "models", `${encodedProvider}.json`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function fetchModelsDev<TApi extends Api, TModelsDevPayload>(
|
|
162
|
+
options: ModelManagerOptions<TApi, TModelsDevPayload>,
|
|
163
|
+
): Promise<Model<TApi>[] | null> {
|
|
164
|
+
if (!options.modelsDev) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const payload = await options.modelsDev.fetch();
|
|
170
|
+
return normalizeModelList<TApi>(options.modelsDev.map(payload, options.providerId));
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function fetchDynamicModels<TApi extends Api>(
|
|
177
|
+
fetcher: () => Promise<readonly Model<TApi>[] | null>,
|
|
178
|
+
): Promise<Model<TApi>[] | null> {
|
|
179
|
+
try {
|
|
180
|
+
const models = await fetcher();
|
|
181
|
+
if (models === null) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return normalizeModelList<TApi>(models);
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function shouldFetchRemoteSources(
|
|
191
|
+
strategy: ModelRefreshStrategy,
|
|
192
|
+
hasFreshCache: boolean,
|
|
193
|
+
hasAuthoritativeCache: boolean,
|
|
194
|
+
): boolean {
|
|
195
|
+
if (strategy === "offline") {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
if (strategy === "online") {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
return !hasFreshCache || !hasAuthoritativeCache;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function readCache<TApi extends Api>(
|
|
205
|
+
cachePath: string,
|
|
206
|
+
expectedProviderId: string,
|
|
207
|
+
ttlMs: number,
|
|
208
|
+
now: () => number,
|
|
209
|
+
): Promise<CacheReadResult<TApi> | null> {
|
|
210
|
+
let raw: string;
|
|
211
|
+
try {
|
|
212
|
+
raw = await Bun.file(cachePath).text();
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (isEnoent(error)) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let parsed: unknown;
|
|
221
|
+
try {
|
|
222
|
+
parsed = JSON.parse(raw);
|
|
223
|
+
} catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const cache = parseCache<TApi>(parsed);
|
|
228
|
+
if (!cache || cache.providerId !== expectedProviderId) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const ageMs = now() - cache.updatedAt;
|
|
233
|
+
const fresh = Number.isFinite(ageMs) && ageMs >= 0 && ageMs <= ttlMs;
|
|
234
|
+
return {
|
|
235
|
+
models: cache.models,
|
|
236
|
+
fresh,
|
|
237
|
+
authoritative: cache.authoritative,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function parseCache<TApi extends Api>(value: unknown): CachedProviderModels<TApi> | null {
|
|
242
|
+
if (!isRecord(value)) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
if (value.version !== CACHE_SCHEMA_VERSION) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
if (typeof value.providerId !== "string") {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
if (typeof value.updatedAt !== "number" || !Number.isFinite(value.updatedAt)) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const rawModels = Array.isArray(value.models)
|
|
255
|
+
? value.models
|
|
256
|
+
: Array.isArray(value.dynamicModels)
|
|
257
|
+
? value.dynamicModels
|
|
258
|
+
: null;
|
|
259
|
+
if (!rawModels) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
const authoritative =
|
|
263
|
+
typeof value.authoritative === "boolean" ? value.authoritative : Array.isArray(value.dynamicModels);
|
|
264
|
+
return {
|
|
265
|
+
version: value.version,
|
|
266
|
+
providerId: value.providerId,
|
|
267
|
+
updatedAt: value.updatedAt,
|
|
268
|
+
models: normalizeModelList<TApi>(rawModels),
|
|
269
|
+
authoritative,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function writeCache<TApi extends Api>(cachePath: string, cache: CachedProviderModels<TApi>): Promise<void> {
|
|
274
|
+
const content = `${JSON.stringify(cache, null, 2)}\n`;
|
|
275
|
+
try {
|
|
276
|
+
await Bun.write(cachePath, content);
|
|
277
|
+
await fs.chmod(cachePath, 0o600).catch(() => undefined);
|
|
278
|
+
} catch {
|
|
279
|
+
// Cache writes are best-effort; failures should not break model resolution.
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function mergeModelSources<TApi extends Api>(...sources: readonly (readonly Model<TApi>[])[]): Model<TApi>[] {
|
|
284
|
+
const merged = new Map<string, Model<TApi>>();
|
|
285
|
+
for (const source of sources) {
|
|
286
|
+
for (const model of source) {
|
|
287
|
+
if (!model?.id) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
merged.set(model.id, model);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return Array.from(merged.values());
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function mergeDynamicModels<TApi extends Api>(
|
|
297
|
+
baseModels: readonly Model<TApi>[],
|
|
298
|
+
dynamicModels: readonly Model<TApi>[],
|
|
299
|
+
): Model<TApi>[] {
|
|
300
|
+
const merged = new Map<string, Model<TApi>>(baseModels.map(model => [model.id, model]));
|
|
301
|
+
for (const dynamicModel of dynamicModels) {
|
|
302
|
+
if (!dynamicModel?.id) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const existingModel = merged.get(dynamicModel.id);
|
|
306
|
+
if (!existingModel) {
|
|
307
|
+
merged.set(dynamicModel.id, dynamicModel);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
merged.set(dynamicModel.id, mergeDynamicModel(existingModel, dynamicModel));
|
|
311
|
+
}
|
|
312
|
+
return Array.from(merged.values());
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamicModel: Model<TApi>): Model<TApi> {
|
|
316
|
+
const supportsImage = existingModel.input.includes("image") || dynamicModel.input.includes("image");
|
|
317
|
+
return {
|
|
318
|
+
...existingModel,
|
|
319
|
+
...dynamicModel,
|
|
320
|
+
name: preferDiscoveryName(dynamicModel.name, existingModel.name, dynamicModel.id),
|
|
321
|
+
reasoning: existingModel.reasoning || dynamicModel.reasoning,
|
|
322
|
+
input: supportsImage ? ["text", "image"] : ["text"],
|
|
323
|
+
cost: {
|
|
324
|
+
input: preferDiscoveryCost(dynamicModel.cost.input, existingModel.cost.input),
|
|
325
|
+
output: preferDiscoveryCost(dynamicModel.cost.output, existingModel.cost.output),
|
|
326
|
+
cacheRead: preferDiscoveryCost(dynamicModel.cost.cacheRead, existingModel.cost.cacheRead),
|
|
327
|
+
cacheWrite: preferDiscoveryCost(dynamicModel.cost.cacheWrite, existingModel.cost.cacheWrite),
|
|
328
|
+
},
|
|
329
|
+
contextWindow: preferDiscoveryLimit(dynamicModel.contextWindow, existingModel.contextWindow),
|
|
330
|
+
maxTokens: preferDiscoveryLimit(dynamicModel.maxTokens, existingModel.maxTokens),
|
|
331
|
+
headers: dynamicModel.headers ? { ...existingModel.headers, ...dynamicModel.headers } : existingModel.headers,
|
|
332
|
+
compat: dynamicModel.compat ?? existingModel.compat,
|
|
333
|
+
contextPromotionTarget: dynamicModel.contextPromotionTarget ?? existingModel.contextPromotionTarget,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function preferDiscoveryCost(discoveryCost: number, fallbackCost: number): number {
|
|
338
|
+
if (Number.isFinite(discoveryCost) && discoveryCost > 0) {
|
|
339
|
+
return discoveryCost;
|
|
340
|
+
}
|
|
341
|
+
return fallbackCost;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function preferDiscoveryName(discoveryName: string, fallbackName: string, modelId: string): string {
|
|
345
|
+
const normalizedDiscoveryName = discoveryName.trim();
|
|
346
|
+
if (normalizedDiscoveryName.length === 0) {
|
|
347
|
+
return fallbackName;
|
|
348
|
+
}
|
|
349
|
+
if (normalizedDiscoveryName === modelId && fallbackName !== modelId) {
|
|
350
|
+
return fallbackName;
|
|
351
|
+
}
|
|
352
|
+
return normalizedDiscoveryName;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function preferDiscoveryLimit(discoveryLimit: number, fallbackLimit: number): number {
|
|
356
|
+
if (!Number.isFinite(discoveryLimit) || discoveryLimit <= 0) {
|
|
357
|
+
return fallbackLimit;
|
|
358
|
+
}
|
|
359
|
+
if (discoveryLimit === 4096 && fallbackLimit > discoveryLimit) {
|
|
360
|
+
return fallbackLimit;
|
|
361
|
+
}
|
|
362
|
+
return discoveryLimit;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function normalizeModelList<TApi extends Api>(value: unknown): Model<TApi>[] {
|
|
366
|
+
if (!Array.isArray(value)) {
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
const models: Model<TApi>[] = [];
|
|
370
|
+
for (const item of value) {
|
|
371
|
+
if (isModelLike(item)) {
|
|
372
|
+
models.push(item as Model<TApi>);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return models;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function isModelLike(value: unknown): value is Model<Api> {
|
|
379
|
+
if (!isRecord(value)) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
if (typeof value.id !== "string" || value.id.length === 0) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
if (typeof value.name !== "string" || value.name.length === 0) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
if (typeof value.api !== "string" || value.api.length === 0) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
if (typeof value.provider !== "string" || value.provider.length === 0) {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
if (typeof value.baseUrl !== "string" || value.baseUrl.length === 0) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
if (typeof value.reasoning !== "boolean") {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
if (!isModelInputArray(value.input)) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
if (!isModelCost(value.cost)) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
if (typeof value.contextWindow !== "number" || !Number.isFinite(value.contextWindow) || value.contextWindow <= 0) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
if (typeof value.maxTokens !== "number" || !Number.isFinite(value.maxTokens) || value.maxTokens <= 0) {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
416
|
+
return typeof value === "object" && value !== null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function isModelInputArray(value: unknown): value is ("text" | "image")[] {
|
|
420
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
return value.every(item => item === "text" || item === "image");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function isModelCost(value: unknown): value is Model<Api>["cost"] {
|
|
427
|
+
if (!isRecord(value)) {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
return (
|
|
431
|
+
typeof value.input === "number" &&
|
|
432
|
+
Number.isFinite(value.input) &&
|
|
433
|
+
typeof value.output === "number" &&
|
|
434
|
+
Number.isFinite(value.output) &&
|
|
435
|
+
typeof value.cacheRead === "number" &&
|
|
436
|
+
Number.isFinite(value.cacheRead) &&
|
|
437
|
+
typeof value.cacheWrite === "number" &&
|
|
438
|
+
Number.isFinite(value.cacheWrite)
|
|
439
|
+
);
|
|
440
|
+
}
|