@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/types/auth-storage.d.ts +2 -2
  3. package/dist/types/model-cache.d.ts +2 -0
  4. package/dist/types/provider-models/special.d.ts +3 -1
  5. package/dist/types/providers/anthropic.d.ts +1 -0
  6. package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
  7. package/dist/types/providers/cursor/gen/agent_pb.d.ts +3854 -107
  8. package/dist/types/providers/cursor-pi-args.d.ts +119 -0
  9. package/dist/types/providers/cursor.d.ts +8 -1
  10. package/dist/types/providers/openai-codex-responses.d.ts +2 -0
  11. package/dist/types/providers/openai-responses-shared.d.ts +1 -1
  12. package/dist/types/types.d.ts +41 -1
  13. package/dist/types/utils/block-symbols.d.ts +6 -0
  14. package/dist/types/utils/discovery/openai-compatible.d.ts +2 -0
  15. package/dist/types/utils/idle-iterator.d.ts +5 -2
  16. package/dist/types/utils/oauth/kimi.d.ts +3 -9
  17. package/dist/types/utils/oauth/openrouter.d.ts +1 -0
  18. package/dist/types/utils/oauth/types.d.ts +1 -1
  19. package/package.json +4 -4
  20. package/src/auth-broker/remote-store.ts +13 -2
  21. package/src/auth-storage.ts +10 -11
  22. package/src/model-cache.ts +78 -0
  23. package/src/model-manager.ts +194 -25
  24. package/src/provider-models/special.ts +67 -4
  25. package/src/providers/anthropic.ts +115 -40
  26. package/src/providers/aws-credential-config.ts +2 -3
  27. package/src/providers/aws-credentials.ts +2 -3
  28. package/src/providers/azure-openai-responses.ts +18 -2
  29. package/src/providers/cursor/exec-modern.ts +497 -0
  30. package/src/providers/cursor/gen/agent_pb.ts +4687 -181
  31. package/src/providers/cursor/proto/agent.proto +1007 -0
  32. package/src/providers/cursor-pi-args.ts +187 -0
  33. package/src/providers/cursor.ts +382 -47
  34. package/src/providers/google-auth.ts +2 -3
  35. package/src/providers/openai-codex-responses.ts +358 -73
  36. package/src/providers/openai-completions.ts +2 -2
  37. package/src/providers/openai-responses-shared.ts +55 -6
  38. package/src/providers/openai-responses.ts +27 -4
  39. package/src/stream.ts +8 -3
  40. package/src/types.ts +55 -0
  41. package/src/utils/block-symbols.ts +11 -0
  42. package/src/utils/discovery/openai-compatible.ts +21 -6
  43. package/src/utils/idle-iterator.ts +22 -4
  44. package/src/utils/oauth/index.ts +6 -0
  45. package/src/utils/oauth/kimi.ts +14 -8
  46. package/src/utils/oauth/kiro.ts +2 -2
  47. package/src/utils/oauth/openrouter.ts +16 -0
  48. package/src/utils/oauth/types.ts +1 -0
@@ -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 (typeof candidate.id !== "string") {
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
- out.push(enrichModelThinking(item as Model<TApi>));
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 cacheProvenanceMismatch = cache?.dynamicModelIds !== undefined && !cacheDynamicModelIdsCurrent;
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) && (cache?.dynamicModelIds === undefined || cacheDynamicModelIdsCurrent));
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
- cacheProvenanceMismatch && strategy !== "offline"
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
- const cacheModels = dynamicFetchSucceeded ? [] : normalizeModelList<TApi>(cache?.models ?? []);
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 = !hasDynamicFetcher || dynamicFetchSucceeded || shouldUseFreshCacheAsAuthoritative;
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
- writeModelCache(
237
- options.providerId,
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: preferDiscoveryName(dynamicModel.name, existingModel.name, dynamicModel.id),
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 (typeof v.id !== "string" || v.id.length === 0) {
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 { once } from "@gajae-code/utils";
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
- _config: GlmZcodeModelManagerConfig = {},
92
+ config: GlmZcodeModelManagerConfig = {},
85
93
  ): ModelManagerOptions<"anthropic-messages"> {
86
- return { providerId: "glm-zcode" };
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)