@gajae-code/ai 0.14.2 → 0.15.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/CHANGELOG.md +26 -0
- package/dist/types/auth-storage.d.ts +2 -2
- package/dist/types/model-cache.d.ts +2 -0
- package/dist/types/provider-models/special.d.ts +3 -1
- package/dist/types/providers/anthropic.d.ts +1 -0
- package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
- package/dist/types/providers/cursor/gen/agent_pb.d.ts +3854 -107
- package/dist/types/providers/cursor-pi-args.d.ts +119 -0
- package/dist/types/providers/cursor.d.ts +8 -1
- package/dist/types/providers/openai-codex-responses.d.ts +2 -0
- package/dist/types/providers/openai-responses-shared.d.ts +1 -1
- package/dist/types/types.d.ts +41 -1
- package/dist/types/utils/block-symbols.d.ts +6 -0
- package/dist/types/utils/discovery/openai-compatible.d.ts +2 -0
- package/dist/types/utils/idle-iterator.d.ts +5 -2
- package/dist/types/utils/oauth/kimi.d.ts +3 -9
- package/dist/types/utils/oauth/openrouter.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/package.json +4 -4
- package/src/auth-broker/remote-store.ts +13 -2
- package/src/auth-storage.ts +10 -11
- package/src/model-cache.ts +78 -0
- package/src/model-manager.ts +194 -25
- package/src/provider-models/special.ts +67 -4
- package/src/providers/anthropic.ts +115 -40
- package/src/providers/aws-credential-config.ts +2 -3
- package/src/providers/aws-credentials.ts +2 -3
- package/src/providers/azure-openai-responses.ts +18 -2
- package/src/providers/cursor/exec-modern.ts +497 -0
- package/src/providers/cursor/gen/agent_pb.ts +4687 -181
- package/src/providers/cursor/proto/agent.proto +1007 -0
- package/src/providers/cursor-pi-args.ts +187 -0
- package/src/providers/cursor.ts +382 -47
- package/src/providers/google-auth.ts +2 -3
- package/src/providers/openai-codex-responses.ts +358 -73
- package/src/providers/openai-completions.ts +2 -2
- package/src/providers/openai-responses-shared.ts +55 -6
- package/src/providers/openai-responses.ts +27 -4
- package/src/stream.ts +8 -3
- package/src/types.ts +55 -0
- package/src/utils/block-symbols.ts +11 -0
- package/src/utils/discovery/openai-compatible.ts +21 -6
- package/src/utils/idle-iterator.ts +22 -4
- package/src/utils/oauth/index.ts +6 -0
- package/src/utils/oauth/kimi.ts +14 -8
- package/src/utils/oauth/kiro.ts +2 -2
- package/src/utils/oauth/openrouter.ts +16 -0
- package/src/utils/oauth/types.ts +1 -0
package/src/model-manager.ts
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
|
+
import { sanitizeText } from "@gajae-code/utils";
|
|
1
2
|
import { applyFinalCodexGpt56ContextCap } from "./context-cap-policy";
|
|
2
|
-
import { readModelCache, writeModelCache } from "./model-cache";
|
|
3
|
+
import { insertModelCacheIfAbsent, readModelCache, updateModelCacheIfUnchanged, writeModelCache } from "./model-cache";
|
|
3
4
|
import { isRetiredModel, isRetiredModelKey } from "./model-retirements";
|
|
4
5
|
import { applyGeneratedModelPolicies, enrichModelThinking } from "./model-thinking";
|
|
5
6
|
import { type GeneratedProvider, getBundledModels } from "./models";
|
|
6
7
|
import type { Api, Model, Provider } from "./types";
|
|
8
|
+
import { isSafeCatalogModelId } from "./utils/discovery/openai-compatible";
|
|
7
9
|
|
|
8
10
|
const DEFAULT_CACHE_TTL_MS = 2 * 60 * 60 * 1000;
|
|
9
11
|
const NON_AUTHORITATIVE_RETRY_MS = 5 * 60 * 1000;
|
|
10
12
|
|
|
13
|
+
// Coalescing guards the implicit cold-start strategy only: an explicit
|
|
14
|
+
// "online" refresh owns its fetch so a newer probe can always supersede an
|
|
15
|
+
// older in-flight refresh (the registry pins that ordering), and callers never
|
|
16
|
+
// block behind a refresh whose completion they may be expected to unblock.
|
|
17
|
+
// A legacy or missing row has no dynamic-ID marker, so concurrent cold-start
|
|
18
|
+
// resolutions would otherwise all fetch before the first one can publish it.
|
|
19
|
+
const legacyDynamicRefreshes = new Map<string, Promise<void>>();
|
|
20
|
+
|
|
21
|
+
function legacyDynamicRefreshKey(providerId: Provider, cacheDbPath: string | undefined, provenance: string): string {
|
|
22
|
+
return `${cacheDbPath ?? "<default>"}\0${providerId}\0${provenance}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
/**
|
|
12
26
|
* Controls when dynamic endpoint models should be fetched.
|
|
13
27
|
*/
|
|
@@ -105,13 +119,14 @@ function passModelList<TApi extends Api>(value: unknown): Model<TApi>[] {
|
|
|
105
119
|
continue;
|
|
106
120
|
}
|
|
107
121
|
const candidate = item as { id?: unknown; provider?: unknown };
|
|
108
|
-
if (
|
|
122
|
+
if (!isSafeCatalogModelId(candidate.id)) {
|
|
109
123
|
continue;
|
|
110
124
|
}
|
|
111
125
|
if (typeof candidate.provider === "string" && isRetiredModelKey(candidate.provider, candidate.id)) {
|
|
112
126
|
continue;
|
|
113
127
|
}
|
|
114
|
-
|
|
128
|
+
const model = enrichModelThinking(item as Model<TApi>);
|
|
129
|
+
out.push({ ...model, name: sanitizeModelDisplayName(model.name, model.id) });
|
|
115
130
|
}
|
|
116
131
|
applyGeneratedModelPolicies(out as Model<Api>[]);
|
|
117
132
|
return applyFinalCodexGpt56ContextCap(out);
|
|
@@ -126,6 +141,42 @@ function passModelList<TApi extends Api>(value: unknown): Model<TApi>[] {
|
|
|
126
141
|
export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPayload = unknown>(
|
|
127
142
|
options: ModelManagerOptions<TApi, TModelsDevPayload>,
|
|
128
143
|
strategy: ModelRefreshStrategy = "online-if-uncached",
|
|
144
|
+
): Promise<ModelResolutionResult<TApi>> {
|
|
145
|
+
const provenance = options.cacheDynamicModelProvenance;
|
|
146
|
+
const now = options.now ?? Date.now;
|
|
147
|
+
const ttlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
148
|
+
const legacyCache = readModelCache<TApi>(options.providerId, ttlMs, now, options.cacheDbPath);
|
|
149
|
+
if (
|
|
150
|
+
strategy !== "online-if-uncached" ||
|
|
151
|
+
typeof options.fetchDynamicModels !== "function" ||
|
|
152
|
+
provenance === undefined ||
|
|
153
|
+
(legacyCache !== null && legacyCache.dynamicModelIds !== undefined)
|
|
154
|
+
) {
|
|
155
|
+
return resolveProviderModelsUncoalesced(options, strategy);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const refreshKey = legacyDynamicRefreshKey(options.providerId, options.cacheDbPath, provenance);
|
|
159
|
+
const inFlightRefresh = legacyDynamicRefreshes.get(refreshKey);
|
|
160
|
+
if (inFlightRefresh) {
|
|
161
|
+
await inFlightRefresh;
|
|
162
|
+
return resolveProviderModelsUncoalesced(options, strategy);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const refreshCompletion = Promise.withResolvers<void>();
|
|
166
|
+
legacyDynamicRefreshes.set(refreshKey, refreshCompletion.promise);
|
|
167
|
+
try {
|
|
168
|
+
return await resolveProviderModelsUncoalesced(options, strategy);
|
|
169
|
+
} finally {
|
|
170
|
+
if (legacyDynamicRefreshes.get(refreshKey) === refreshCompletion.promise) {
|
|
171
|
+
legacyDynamicRefreshes.delete(refreshKey);
|
|
172
|
+
}
|
|
173
|
+
refreshCompletion.resolve();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function resolveProviderModelsUncoalesced<TApi extends Api = Api, TModelsDevPayload = unknown>(
|
|
178
|
+
options: ModelManagerOptions<TApi, TModelsDevPayload>,
|
|
179
|
+
strategy: ModelRefreshStrategy = "online-if-uncached",
|
|
129
180
|
): Promise<ModelResolutionResult<TApi>> {
|
|
130
181
|
const now = options.now ?? Date.now;
|
|
131
182
|
const ttlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
@@ -140,13 +191,37 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
140
191
|
cache?.dynamicModelIds !== undefined &&
|
|
141
192
|
cache.dynamicModelProvenance !== undefined &&
|
|
142
193
|
cache.dynamicModelProvenance === options.cacheDynamicModelProvenance;
|
|
143
|
-
const
|
|
194
|
+
const requiresBoundDynamicCache = hasDynamicFetcher && options.cacheDynamicModelProvenance !== undefined;
|
|
195
|
+
const cacheProvenanceMismatch =
|
|
196
|
+
cache !== null &&
|
|
197
|
+
(cache.dynamicModelIds !== undefined ? !cacheDynamicModelIdsCurrent : requiresBoundDynamicCache);
|
|
198
|
+
// A provider that supplies cache provenance has opted into live-catalog
|
|
199
|
+
// authority. Rows written before that provider enabled discovery have no
|
|
200
|
+
// dynamic IDs, so they must be synchronized once rather than suppressing
|
|
201
|
+
// the first live fetch for their full TTL.
|
|
202
|
+
const cacheNeedsInitialDynamicRefresh =
|
|
203
|
+
hasDynamicFetcher &&
|
|
204
|
+
options.cacheDynamicModelProvenance !== undefined &&
|
|
205
|
+
!options.cacheDynamicModelProvenance.startsWith("gajae:non-cacheable-") &&
|
|
206
|
+
cache?.dynamicModelIds === undefined;
|
|
144
207
|
const hasAuthoritativeCache =
|
|
145
208
|
!hasDynamicFetcher ||
|
|
146
|
-
((cache?.authoritative ?? false) &&
|
|
209
|
+
((cache?.authoritative ?? false) &&
|
|
210
|
+
(!cacheNeedsInitialDynamicRefresh && requiresBoundDynamicCache
|
|
211
|
+
? cacheDynamicModelIdsCurrent
|
|
212
|
+
: cache?.dynamicModelIds === undefined || cacheDynamicModelIdsCurrent));
|
|
147
213
|
const cacheAgeMs = cache ? now() - cache.updatedAt : Number.POSITIVE_INFINITY;
|
|
148
214
|
const shouldFetchFromNetwork =
|
|
149
|
-
|
|
215
|
+
// A bound row whose dynamic IDs are defined but stale belongs to a
|
|
216
|
+
// different discovery context: force the validation fetch immediately,
|
|
217
|
+
// on every non-offline visit, until a row for this context lands.
|
|
218
|
+
// A row with no dynamic IDs (a failed-fetch tombstone, or a legacy
|
|
219
|
+
// provenance-less row) cannot prove foreignness, never serves its
|
|
220
|
+
// models in a bound context, and is non-authoritative — so its refetch
|
|
221
|
+
// cadence follows the standard non-authoritative retry backoff instead
|
|
222
|
+
// of hammering a failing endpoint on every provider-tab visit.
|
|
223
|
+
(cacheNeedsInitialDynamicRefresh || (cacheProvenanceMismatch && cache?.dynamicModelIds !== undefined)) &&
|
|
224
|
+
strategy !== "offline"
|
|
150
225
|
? true
|
|
151
226
|
: shouldFetchRemoteSources(strategy, cache?.fresh ?? false, hasAuthoritativeCache, cacheAgeMs);
|
|
152
227
|
const staticFingerprint = fingerprintStatic(staticModels);
|
|
@@ -206,11 +281,24 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
206
281
|
const shouldUseFreshCacheAsAuthoritative =
|
|
207
282
|
strategy === "online-if-uncached" && (cache?.fresh ?? false) && hasAuthoritativeCache;
|
|
208
283
|
const dynamicFetchSucceeded = fetchedDynamicModels !== null;
|
|
209
|
-
|
|
284
|
+
// Stale-while-error fallback: cached dynamic rows may only serve when their
|
|
285
|
+
// provenance still matches the current request context. A fetch forced by a
|
|
286
|
+
// provenance mismatch means the cache belongs to a different discovery
|
|
287
|
+
// context (e.g. another tenant's header), so a failed refetch must fail
|
|
288
|
+
// closed on those rows instead of surfacing them as selectable models. The
|
|
289
|
+
// explicit "offline" strategy keeps serving last-known rows regardless:
|
|
290
|
+
// local-only mode has no network attempt whose failure would warrant
|
|
291
|
+
// withholding them.
|
|
292
|
+
const cacheModelsServeCurrentContext = !cacheProvenanceMismatch || strategy === "offline";
|
|
293
|
+
let cacheModels =
|
|
294
|
+
dynamicFetchSucceeded || !cacheModelsServeCurrentContext ? [] : normalizeModelList<TApi>(cache?.models ?? []);
|
|
210
295
|
const dynamicModels = fetchedDynamicModels ?? [];
|
|
211
296
|
const mergedWithCache = mergeDynamicModels(mergeModelSources(staticModels, modelsDevModels), cacheModels);
|
|
212
297
|
const models = applyFinalCodexGpt56ContextCap(mergeDynamicModels(mergedWithCache, dynamicModels));
|
|
213
|
-
const dynamicAuthoritative =
|
|
298
|
+
const dynamicAuthoritative =
|
|
299
|
+
!hasDynamicFetcher ||
|
|
300
|
+
dynamicFetchSucceeded ||
|
|
301
|
+
(shouldUseFreshCacheAsAuthoritative && cacheModelsServeCurrentContext);
|
|
214
302
|
if (shouldFetchFromNetwork) {
|
|
215
303
|
if (dynamicFetchSucceeded) {
|
|
216
304
|
const snapshotModels = applyFinalCodexGpt56ContextCap(
|
|
@@ -230,27 +318,73 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
230
318
|
}
|
|
231
319
|
} else {
|
|
232
320
|
// Dynamic fetch failed — update cache with a non-authoritative snapshot so
|
|
233
|
-
// stale state remains visible while retry backoff still applies.
|
|
321
|
+
// stale state remains visible while retry backoff still applies. Re-read
|
|
322
|
+
// the current row and trust only that row's provenance and dynamic IDs,
|
|
323
|
+
// not the initial cacheProvenanceMismatch snapshot. A concurrent writer
|
|
324
|
+
// may have replaced the provider row; if the latest row is unbound or
|
|
325
|
+
// belongs to another context, use no latest fallback and do not
|
|
326
|
+
// overwrite or downgrade it.
|
|
234
327
|
const latestCache = readModelCache<TApi>(options.providerId, ttlMs, now, dbPath);
|
|
328
|
+
const latestCacheMatchesCurrentContext = cacheRowMatchesBoundDynamicProvenance(
|
|
329
|
+
latestCache,
|
|
330
|
+
options.cacheDynamicModelProvenance,
|
|
331
|
+
);
|
|
332
|
+
const latestCacheIsLegacy =
|
|
333
|
+
cache?.dynamicModelIds === undefined && latestCache !== null && latestCache.dynamicModelIds === undefined;
|
|
334
|
+
// An unbound context (the provider supplies no dynamic provenance) has no
|
|
335
|
+
// foreign-context risk, so a legacy row keeps serving its models through a
|
|
336
|
+
// failed refetch instead of blanking the catalog and overwriting the row
|
|
337
|
+
// with an empty snapshot.
|
|
338
|
+
const latestCacheServesLegacyRow = latestCacheIsLegacy && options.cacheDynamicModelProvenance === undefined;
|
|
339
|
+
const fallbackCacheModels =
|
|
340
|
+
(latestCacheMatchesCurrentContext || latestCacheServesLegacyRow) && latestCache !== null
|
|
341
|
+
? normalizeModelList<TApi>(latestCache.models)
|
|
342
|
+
: [];
|
|
343
|
+
cacheModels = fallbackCacheModels;
|
|
235
344
|
if (options.canPublishCache?.() ?? true) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
now(),
|
|
239
|
-
applyFinalCodexGpt56ContextCap(
|
|
240
|
-
mergeDynamicModels(
|
|
241
|
-
mergeModelSources(staticModels, modelsDevModels),
|
|
242
|
-
normalizeModelList<TApi>(latestCache?.models ?? cache?.models ?? []),
|
|
243
|
-
),
|
|
244
|
-
),
|
|
245
|
-
false,
|
|
246
|
-
staticFingerprint,
|
|
247
|
-
dbPath,
|
|
345
|
+
const snapshotModels = applyFinalCodexGpt56ContextCap(
|
|
346
|
+
mergeDynamicModels(mergeModelSources(staticModels, modelsDevModels), fallbackCacheModels),
|
|
248
347
|
);
|
|
348
|
+
if (latestCacheMatchesCurrentContext || latestCacheIsLegacy) {
|
|
349
|
+
const updated = updateModelCacheIfUnchanged(
|
|
350
|
+
options.providerId,
|
|
351
|
+
latestCache.updatedAt,
|
|
352
|
+
latestCache.dynamicModelIds,
|
|
353
|
+
latestCache.dynamicModelProvenance,
|
|
354
|
+
latestCache.models,
|
|
355
|
+
now(),
|
|
356
|
+
snapshotModels,
|
|
357
|
+
false,
|
|
358
|
+
staticFingerprint,
|
|
359
|
+
dbPath,
|
|
360
|
+
options.cacheDynamicModelProvenance === undefined ? undefined : [],
|
|
361
|
+
options.cacheDynamicModelProvenance,
|
|
362
|
+
);
|
|
363
|
+
if (!updated) cacheModels = [];
|
|
364
|
+
} else if (latestCache == null) {
|
|
365
|
+
const inserted = insertModelCacheIfAbsent(
|
|
366
|
+
options.providerId,
|
|
367
|
+
now(),
|
|
368
|
+
snapshotModels,
|
|
369
|
+
false,
|
|
370
|
+
staticFingerprint,
|
|
371
|
+
dbPath,
|
|
372
|
+
options.cacheDynamicModelProvenance === undefined ? undefined : [],
|
|
373
|
+
options.cacheDynamicModelProvenance,
|
|
374
|
+
);
|
|
375
|
+
if (!inserted) cacheModels = [];
|
|
376
|
+
}
|
|
249
377
|
}
|
|
250
378
|
}
|
|
251
379
|
}
|
|
380
|
+
const returnedModels =
|
|
381
|
+
shouldFetchFromNetwork && !dynamicFetchSucceeded
|
|
382
|
+
? applyFinalCodexGpt56ContextCap(
|
|
383
|
+
mergeDynamicModels(mergeModelSources(staticModels, modelsDevModels), cacheModels),
|
|
384
|
+
)
|
|
385
|
+
: models;
|
|
252
386
|
return {
|
|
253
|
-
models,
|
|
387
|
+
models: returnedModels,
|
|
254
388
|
stale: !dynamicAuthoritative,
|
|
255
389
|
cacheFresh: cache?.fresh ?? false,
|
|
256
390
|
cacheAuthoritative: cache?.authoritative ?? false,
|
|
@@ -294,6 +428,29 @@ async function fetchDynamicModels<TApi extends Api>(
|
|
|
294
428
|
}
|
|
295
429
|
}
|
|
296
430
|
|
|
431
|
+
function cacheRowMatchesBoundDynamicProvenance<
|
|
432
|
+
TCache extends {
|
|
433
|
+
models: readonly { id: string }[];
|
|
434
|
+
dynamicModelIds?: readonly string[];
|
|
435
|
+
dynamicModelProvenance?: string;
|
|
436
|
+
},
|
|
437
|
+
>(
|
|
438
|
+
cache: TCache | null | undefined,
|
|
439
|
+
expectedProvenance: string | undefined,
|
|
440
|
+
): cache is TCache & { dynamicModelIds: readonly string[]; dynamicModelProvenance: string } {
|
|
441
|
+
if (
|
|
442
|
+
cache == null ||
|
|
443
|
+
cache.dynamicModelIds === undefined ||
|
|
444
|
+
cache.dynamicModelProvenance === undefined ||
|
|
445
|
+
expectedProvenance === undefined ||
|
|
446
|
+
cache.dynamicModelProvenance !== expectedProvenance
|
|
447
|
+
) {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
const modelIds = new Set(cache.models.map(model => model.id));
|
|
451
|
+
return cache.dynamicModelIds.every(id => modelIds.has(id));
|
|
452
|
+
}
|
|
453
|
+
|
|
297
454
|
function shouldFetchRemoteSources(
|
|
298
455
|
strategy: ModelRefreshStrategy,
|
|
299
456
|
hasFreshCache: boolean,
|
|
@@ -410,7 +567,10 @@ function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamic
|
|
|
410
567
|
...dynamicModel,
|
|
411
568
|
api: existingModel.api,
|
|
412
569
|
baseUrl,
|
|
413
|
-
name:
|
|
570
|
+
name: sanitizeModelDisplayName(
|
|
571
|
+
preferDiscoveryName(dynamicModel.name, existingModel.name, dynamicModel.id),
|
|
572
|
+
dynamicModel.id,
|
|
573
|
+
),
|
|
414
574
|
reasoning: existingModel.reasoning || dynamicModel.reasoning,
|
|
415
575
|
input: supportsImage ? ["text", "image"] : ["text"],
|
|
416
576
|
cost: {
|
|
@@ -448,6 +608,15 @@ function preferDiscoveryName(discoveryName: string, fallbackName: string, modelI
|
|
|
448
608
|
return normalizedDiscoveryName;
|
|
449
609
|
}
|
|
450
610
|
|
|
611
|
+
const MODEL_DISPLAY_NAME_MAX_LENGTH = 200;
|
|
612
|
+
|
|
613
|
+
function sanitizeModelDisplayName(name: string, modelId: string): string {
|
|
614
|
+
const sanitizedName = sanitizeText(name).replace(/\s+/g, " ").trim().slice(0, MODEL_DISPLAY_NAME_MAX_LENGTH);
|
|
615
|
+
if (sanitizedName.length > 0) return sanitizedName;
|
|
616
|
+
const sanitizedId = sanitizeText(modelId).replace(/\s+/g, " ").trim().slice(0, MODEL_DISPLAY_NAME_MAX_LENGTH);
|
|
617
|
+
return sanitizedId || "Unnamed model";
|
|
618
|
+
}
|
|
619
|
+
|
|
451
620
|
function preferDiscoveryLimit(discoveryLimit: number, fallbackLimit: number): number {
|
|
452
621
|
if (!Number.isFinite(discoveryLimit) || discoveryLimit <= 0) {
|
|
453
622
|
return fallbackLimit;
|
|
@@ -467,7 +636,7 @@ function normalizeModelList<TApi extends Api>(value: unknown): Model<TApi>[] {
|
|
|
467
636
|
if (isModelLike(item) && !isRetiredModel(item)) {
|
|
468
637
|
const model = enrichModelThinking(item as Model<TApi>);
|
|
469
638
|
model.longContextPricing = undefined;
|
|
470
|
-
models.push(model);
|
|
639
|
+
models.push({ ...model, name: sanitizeModelDisplayName(model.name, model.id) });
|
|
471
640
|
}
|
|
472
641
|
}
|
|
473
642
|
applyGeneratedModelPolicies(models as Model<Api>[]);
|
|
@@ -490,7 +659,7 @@ function isModelLike(value: unknown): value is Model<Api> {
|
|
|
490
659
|
contextWindow?: unknown;
|
|
491
660
|
maxTokens?: unknown;
|
|
492
661
|
};
|
|
493
|
-
if (
|
|
662
|
+
if (!isSafeCatalogModelId(v.id)) {
|
|
494
663
|
return false;
|
|
495
664
|
}
|
|
496
665
|
if (typeof v.name !== "string" || v.name.length === 0) {
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { UNK_CONTEXT_WINDOW, UNK_MAX_TOKENS } from "@gajae-code/ai";
|
|
2
|
+
import { once, sanitizeText } from "@gajae-code/utils";
|
|
3
|
+
|
|
2
4
|
import type { ModelManagerOptions } from "../model-manager";
|
|
5
|
+
import { buildZCodeSourceHeaders, resolveGlmZcodeAnthropicBaseUrl } from "../providers/anthropic";
|
|
3
6
|
import { fetchOpenCodexModels, OPENCODEX_MODEL_CACHE_TTL_MS } from "../providers/openai-opencodex-responses";
|
|
4
7
|
import { fetchCodexModels } from "../utils/discovery/codex";
|
|
8
|
+
import { fetchOpenAICompatibleModels } from "../utils/discovery/openai-compatible";
|
|
9
|
+
import { createBundledReferenceMap } from "./bundled-references";
|
|
5
10
|
export function openCodexModelManagerOptions(): ModelManagerOptions<"openai-responses"> {
|
|
6
11
|
return {
|
|
7
12
|
providerId: "opencodex",
|
|
@@ -78,12 +83,70 @@ export function zaiModelManagerOptions(_config: ZaiModelManagerConfig = {}): Mod
|
|
|
78
83
|
// GLM ZCode (unofficial Z.AI OAuth)
|
|
79
84
|
// ---------------------------------------------------------------------------
|
|
80
85
|
|
|
81
|
-
export interface GlmZcodeModelManagerConfig {
|
|
86
|
+
export interface GlmZcodeModelManagerConfig {
|
|
87
|
+
apiKey?: string;
|
|
88
|
+
baseUrl?: string;
|
|
89
|
+
}
|
|
82
90
|
|
|
83
91
|
export function glmZcodeModelManagerOptions(
|
|
84
|
-
|
|
92
|
+
config: GlmZcodeModelManagerConfig = {},
|
|
85
93
|
): ModelManagerOptions<"anthropic-messages"> {
|
|
86
|
-
|
|
94
|
+
const apiKey = config.apiKey;
|
|
95
|
+
const baseUrl = resolveGlmZcodeAnthropicBaseUrl();
|
|
96
|
+
const providerRefs = createBundledReferenceMap<"anthropic-messages">("glm-zcode");
|
|
97
|
+
// Same-family GLM references: the thin `glm-zcode` slice bundles only the
|
|
98
|
+
// newest model, while the `zai` slice carries the rest of the GLM family
|
|
99
|
+
// with real capabilities (reasoning, thinking, limits). Resolving through
|
|
100
|
+
// both keeps a newly selectable GLM model from degrading to generic
|
|
101
|
+
// unknown metadata; the provider/base are rewritten below.
|
|
102
|
+
const familyRefs = createBundledReferenceMap<"anthropic-messages">("zai");
|
|
103
|
+
const resolveReference = (modelId: string) => providerRefs.get(modelId) ?? familyRefs.get(modelId);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
providerId: "glm-zcode",
|
|
107
|
+
...(apiKey ? { cacheDynamicModelProvenance: `${Bun.hash(apiKey).toString(36)}\0${baseUrl}` } : undefined),
|
|
108
|
+
...(apiKey
|
|
109
|
+
? {
|
|
110
|
+
fetchDynamicModels: () =>
|
|
111
|
+
fetchOpenAICompatibleModels({
|
|
112
|
+
api: "anthropic-messages",
|
|
113
|
+
provider: "glm-zcode",
|
|
114
|
+
baseUrl: baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`,
|
|
115
|
+
apiKey,
|
|
116
|
+
headers: {
|
|
117
|
+
...buildZCodeSourceHeaders(),
|
|
118
|
+
"anthropic-version": "2023-06-01",
|
|
119
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
120
|
+
},
|
|
121
|
+
mapModel: (entry, defaults) => {
|
|
122
|
+
// The remote catalog is provider-controlled text that ends up
|
|
123
|
+
// rendered in the TUI model selector; strip ANSI/OSC and other
|
|
124
|
+
// control sequences at the discovery boundary.
|
|
125
|
+
const remoteName =
|
|
126
|
+
typeof entry.name === "string" && entry.name.length > 0
|
|
127
|
+
? sanitizeText(entry.name).replace(/\s+/g, " ").trim().slice(0, 200)
|
|
128
|
+
: "";
|
|
129
|
+
const reference = resolveReference(defaults.id);
|
|
130
|
+
if (!reference) {
|
|
131
|
+
return { ...defaults, name: remoteName || defaults.id };
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
...reference,
|
|
135
|
+
id: defaults.id,
|
|
136
|
+
provider: "glm-zcode",
|
|
137
|
+
name: remoteName || reference.name,
|
|
138
|
+
baseUrl,
|
|
139
|
+
contextWindow:
|
|
140
|
+
defaults.contextWindow === UNK_CONTEXT_WINDOW
|
|
141
|
+
? reference.contextWindow
|
|
142
|
+
: defaults.contextWindow,
|
|
143
|
+
maxTokens: defaults.maxTokens === UNK_MAX_TOKENS ? reference.maxTokens : defaults.maxTokens,
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
}
|
|
148
|
+
: undefined),
|
|
149
|
+
};
|
|
87
150
|
}
|
|
88
151
|
// ---------------------------------------------------------------------------
|
|
89
152
|
// JetBrains Junie (JetBrains AI Service, Ingrazzio gateway)
|