@oh-my-pi/pi-catalog 18.2.2 → 18.2.4

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.
@@ -394,6 +394,7 @@ function readRowUncached<TApi extends Api>(
394
394
 
395
395
  /** Whether a live model carries at least one request header. */
396
396
  function hasModelHeaders(model: Model<Api>): boolean {
397
+ if (model.resolveHeaders) return true;
397
398
  const headers = model.headers;
398
399
  if (!headers) return false;
399
400
  for (const _key in headers) return true;
@@ -409,7 +410,7 @@ function hasModelHeaders(model: Model<Api>): boolean {
409
410
  * so a compatible cache can be consumed without running `buildModel`.
410
411
  */
411
412
  function toCachedModel<TApi extends Api>(model: Model<TApi>): PersistedModel<TApi> {
412
- const { headers: _headers, ...rest } = model;
413
+ const { headers: _headers, resolveHeaders: _resolveHeaders, ...rest } = model;
413
414
  return {
414
415
  ...rest,
415
416
  supportsComputerUseConfig: model.supportsComputerUseConfig ?? null,
@@ -458,9 +459,11 @@ export function writeModelCache<TApi extends Api>(
458
459
  // headers equal a trusted provider-wide fallback that the reader can
459
460
  // re-derive without persisting it. This keeps reference-less models
460
461
  // with constant or configured headers alive offline.
461
- const matchesStatic = staticHeaderSource
462
- ? headersEqual(model.headers, staticHeaderSource.headers)
463
- : headersEqual(model.headers, restorableHeaderFallback);
462
+ const matchesStatic = model.resolveHeaders
463
+ ? staticHeaderSource?.resolveHeaders === model.resolveHeaders
464
+ : staticHeaderSource
465
+ ? headersEqual(model.headers, staticHeaderSource.headers)
466
+ : headersEqual(model.headers, restorableHeaderFallback);
464
467
  if (!matchesStatic) {
465
468
  unrestorableHeaderModelIds.push(model.id);
466
469
  }
@@ -59,6 +59,13 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
59
59
  * bearer header comes from the current local config.
60
60
  */
61
61
  restorableHeaderFallback?: Record<string, string>;
62
+ /**
63
+ * Reconstruct omitted request headers from authoritative local configuration
64
+ * when no trusted static source remains. Return undefined to keep the row
65
+ * unavailable. This hook must not perform I/O; attach `resolveHeaders` for
66
+ * command-backed credentials that must wait until an actual request.
67
+ */
68
+ restoreCachedHeaders?: (model: Readonly<Model>) => Pick<Model, "headers" | "resolveHeaders"> | undefined;
62
69
  /** Optional dynamic endpoint fetcher. */
63
70
  fetchDynamicModels?: () => Promise<readonly ModelSpec<TApi>[] | null>;
64
71
  /** Optional stencil.so fallback hook. */
@@ -153,6 +160,7 @@ function restoreCachedModelHeaders<TApi extends Api>(
153
160
  unrestorableHeaderModelIds: readonly string[],
154
161
  legacyHeaderRestoreMarkers: boolean,
155
162
  restorableHeaderFallback: Record<string, string> | undefined,
163
+ restoreCachedHeaders: ModelManagerOptions<TApi>["restoreCachedHeaders"],
156
164
  ): CachedHeaderRestoreResult<TApi> {
157
165
  if (headerOmittedModelIds.length === 0) {
158
166
  return { models: [...cachedModels], unresolvedModelIds: new Set() };
@@ -172,7 +180,7 @@ function restoreCachedModelHeaders<TApi extends Api>(
172
180
  ? staticById.get(model.requestModelId)
173
181
  : undefined
174
182
  : (staticById.get(model.id) ?? (model.requestModelId ? staticById.get(model.requestModelId) : undefined));
175
- if (!staticModel?.headers) {
183
+ if (!staticModel?.headers && !staticModel?.resolveHeaders) {
176
184
  // A non-unrestorable row whose static source is gone was cached with
177
185
  // headers matching the provider's trusted constant (e.g. a Copilot
178
186
  // model with no bundled entry). Reattach the constant by value instead
@@ -180,10 +188,12 @@ function restoreCachedModelHeaders<TApi extends Api>(
180
188
  if (!unrestorable && restorableHeaderFallback) {
181
189
  return { ...model, headers: { ...restorableHeaderFallback } };
182
190
  }
191
+ const configured = restoreCachedHeaders?.(model);
192
+ if (configured?.headers || configured?.resolveHeaders) return { ...model, ...configured };
183
193
  unresolvedModelIds.add(model.id);
184
194
  return model;
185
195
  }
186
- return { ...model, headers: staticModel.headers };
196
+ return { ...model, headers: staticModel.headers, resolveHeaders: staticModel.resolveHeaders };
187
197
  });
188
198
  return { models: restored, unresolvedModelIds };
189
199
  }
@@ -219,6 +229,7 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
219
229
  cache?.unrestorableHeaderModelIds ?? [],
220
230
  cache?.legacyHeaderRestoreMarkers ?? false,
221
231
  restorableHeaderFallback,
232
+ options.restoreCachedHeaders,
222
233
  );
223
234
  const usableCachedModels = restoredCache.models.filter(model => !restoredCache.unresolvedModelIds.has(model.id));
224
235
  const cacheHasUnresolvedHeaders = restoredCache.unresolvedModelIds.size > 0;
@@ -361,6 +372,7 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
361
372
  latestCache?.unrestorableHeaderModelIds ?? cache?.unrestorableHeaderModelIds ?? [],
362
373
  latestCache?.legacyHeaderRestoreMarkers ?? cache?.legacyHeaderRestoreMarkers ?? false,
363
374
  restorableHeaderFallback,
375
+ options.restoreCachedHeaders,
364
376
  );
365
377
  const latestUsableCacheModels = latestRestoredCache.models.filter(
366
378
  model => !latestRestoredCache.unresolvedModelIds.has(model.id),
@@ -582,6 +594,16 @@ function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamic
582
594
  : existingModel.reasoning || dynamicModel.reasoning;
583
595
  const longContextCost = dynamicModel.cost.longContext ?? existingModel.cost.longContext;
584
596
  const timeBasedCost = dynamicModel.cost.timeBased ?? existingModel.cost.timeBased;
597
+ const existingHeaders = existingModel.resolveHeaders ?? existingModel.headers;
598
+ const dynamicHeaders = dynamicModel.resolveHeaders ?? dynamicModel.headers;
599
+ let resolveHeaders = dynamicModel.resolveHeaders ?? existingModel.resolveHeaders;
600
+ if (resolveHeaders && existingHeaders && dynamicHeaders && existingHeaders !== dynamicHeaders) {
601
+ resolveHeaders = async signal => {
602
+ const previous = typeof existingHeaders === "function" ? await existingHeaders(signal) : existingHeaders;
603
+ const next = typeof dynamicHeaders === "function" ? await dynamicHeaders(signal) : dynamicHeaders;
604
+ return { ...previous, ...next };
605
+ };
606
+ }
585
607
  // Re-build from spec stage: sparse compat comes from `compatConfig` (the
586
608
  // verbatim override vocabulary), never the resolved `compat` record.
587
609
  return buildModel({
@@ -600,7 +622,12 @@ function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamic
600
622
  },
601
623
  contextWindow: preferDiscoveryLimit(dynamicModel.contextWindow, existingModel.contextWindow),
602
624
  maxTokens: preferDiscoveryLimit(dynamicModel.maxTokens, existingModel.maxTokens),
603
- headers: dynamicModel.headers ? { ...existingModel.headers, ...dynamicModel.headers } : existingModel.headers,
625
+ headers: resolveHeaders
626
+ ? undefined
627
+ : dynamicModel.headers
628
+ ? { ...existingModel.headers, ...dynamicModel.headers }
629
+ : existingModel.headers,
630
+ resolveHeaders,
604
631
  compat: dynamicModel.compatConfig ?? existingModel.compatConfig,
605
632
  contextPromotionTarget: dynamicModel.contextPromotionTarget ?? existingModel.contextPromotionTarget,
606
633
  } as ModelSpec<TApi>);
package/src/types.ts CHANGED
@@ -1226,6 +1226,12 @@ export interface Model<TApi extends Api = Api> {
1226
1226
  */
1227
1227
  omitMaxOutputTokens?: boolean;
1228
1228
  headers?: Record<string, string>;
1229
+ /**
1230
+ * Materialize config-backed headers immediately before a request. Catalog
1231
+ * inspection never invokes this hook; transports receive a cloned model
1232
+ * whose `headers` is a plain resolved record and whose hook is removed.
1233
+ */
1234
+ resolveHeaders?: (signal?: AbortSignal) => Promise<Record<string, string> | undefined>;
1229
1235
  /**
1230
1236
  * Streaming transport override. When `"pi-native"`, `streamSimple` routes
1231
1237
  * the request to the model's `baseUrl` via the auth-gateway's