@pwguler/pi-pengepul-provider 0.3.0 → 0.4.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/README.md CHANGED
@@ -53,6 +53,11 @@ prefixed `pengepul/<id>`. To try it without installing, use
53
53
  `max_output_tokens`, `input_modalities`, `pricing`). Fields the relay omits
54
54
  fall back to pi's builtin catalog for the same id, then to family
55
55
  heuristics.
56
+ - Names the extended thinking levels (`xhigh`, `max`) that a model's own family
57
+ publishes. An id the catalogs have not caught up with — a point release the
58
+ relay already serves — takes them from the same family's previous minor, so
59
+ it does not sit at `high` until the next pi catalog update. Nothing to
60
+ configure: `thinkingLevelMap` is resolved per id here.
56
61
  - Caches the catalog in pi's model store, so startup does not wait on the
57
62
  network and a briefly absent relay is covered. The cached models are
58
63
  re-pointed at the relay base configured now, so moving the relay does not
@@ -259,6 +259,63 @@ function fallbackLevelMap(id: string): Record<string, string | null> | undefined
259
259
  return id.toLowerCase().startsWith("commandcode/") ? { max: "max" } : undefined
260
260
  }
261
261
 
262
+ /** The levels pi hides unless a map names them. */
263
+ const INHERITED_LEVELS = ["xhigh", "max"] as const
264
+
265
+ /**
266
+ * Extended levels a family's previous minor already carries.
267
+ *
268
+ * pi hides `xhigh` and `max` unless a model's map names them, so an id the
269
+ * catalogs have not caught up with - a Claude point release the relay serves
270
+ * today, before pi's catalog carries it - dropped to low/medium/high and could
271
+ * never send the top of a ladder its own family publishes: in the catalog Opus
272
+ * 4.5 names neither extension, 4.6 names max, and 4.7, 4.8 and 5 name xhigh and
273
+ * max. The previous minor's own entry is that answer, and the only evidence
274
+ * there is: the relay advertises no effort metadata, so `GET /v1/models` has
275
+ * nothing to say about levels and pi's map is the whole vocabulary.
276
+ *
277
+ * Add-only by construction. Only levels the sibling maps to a string come
278
+ * across, so an id that reaches this path can gain the top of the scale but
279
+ * never lose a level that works today, and never takes on a sibling's spelling
280
+ * for off/low/medium/high - the dialect rules downstream own those.
281
+ *
282
+ * Measured against the live catalog at the time of writing: exactly two ids
283
+ * enter this path, `anthropic/claude-opus-5-5` and
284
+ * `openrouter/anthropic/claude-opus-5.5`, each inheriting xhigh and max from
285
+ * `claude-opus-5`. No other family in that catalog was touched.
286
+ */
287
+ function inheritedLevelMap(
288
+ id: string,
289
+ dialect: PengepulDialect,
290
+ lookup: BuiltinModelLookup | undefined,
291
+ ): Record<string, string | null> | undefined {
292
+ const siblingId = previousMinorId(id)
293
+ const siblingMap = siblingId ? lookup?.(siblingId, dialect)?.thinkingLevelMap : undefined
294
+ if (!siblingMap) return undefined
295
+
296
+ const inherited: Record<string, string | null> = {}
297
+ for (const level of INHERITED_LEVELS) {
298
+ const mapped = siblingMap[level]
299
+ if (typeof mapped === "string") inherited[level] = mapped
300
+ }
301
+ return Object.keys(inherited).length > 0 ? inherited : undefined
302
+ }
303
+
304
+ /**
305
+ * The same id one minor older, keeping the routing namespace:
306
+ * `anthropic/claude-opus-5-5` -> `anthropic/claude-opus-5`, and the dotted
307
+ * `claude-opus-5.5` -> `claude-opus-5` for the Chat Completions spelling.
308
+ * Undefined when the name carries no trailing version segment to drop, which
309
+ * keeps qualifiers (`-preview`, `-flash`, `-fast`) out of the derivation.
310
+ */
311
+ function previousMinorId(id: string): string | undefined {
312
+ const slash = id.lastIndexOf("/")
313
+ const name = slash === -1 ? id : id.slice(slash + 1)
314
+ const stripped = name.replace(/[.-]\d+$/, "")
315
+ if (stripped === name || stripped === "") return undefined
316
+ return slash === -1 ? stripped : `${id.slice(0, slash + 1)}${stripped}`
317
+ }
318
+
262
319
  /**
263
320
  * Resolve a model's metadata, most trustworthy source first:
264
321
  * 1. what pengepul advertises (first-party for this relay),
@@ -266,6 +323,11 @@ function fallbackLevelMap(id: string): Record<string, string | null> | undefined
266
323
  * 3. family heuristics.
267
324
  * Sources merge per field, so a relay that sends only `context_window` still
268
325
  * picks up pricing and modalities from the catalog.
326
+ *
327
+ * A catalog miss on a reasoning model still gets its extended levels from the
328
+ * family's previous minor where the catalog carries one (`inheritedLevelMap`);
329
+ * the namespace fallback for the Chat Completions wire then takes precedence
330
+ * over both.
269
331
  */
270
332
  function metaFor(
271
333
  entry: Record<string, unknown>,
@@ -294,7 +356,20 @@ function metaFor(
294
356
  const fallback =
295
357
  known || !reasoning || dialect !== "openai-completions" ? undefined : fallbackLevelMap(id)
296
358
 
297
- return { ...meta, reasoning, ...(fallback ? { thinkingLevelMap: fallback } : {}) }
359
+ // A catalog miss is not always silence: the family's previous minor answers
360
+ // for the levels the catalog has not named yet. Whatever the base resolved
361
+ // wins over it, so a relay that starts advertising a map still outranks this.
362
+ const inherited = known || !reasoning ? undefined : inheritedLevelMap(id, dialect, lookup)
363
+ const levelMap = inherited
364
+ ? { ...inherited, ...(meta.thinkingLevelMap ?? {}) }
365
+ : meta.thinkingLevelMap
366
+
367
+ return {
368
+ ...meta,
369
+ reasoning,
370
+ ...(levelMap ? { thinkingLevelMap: levelMap } : {}),
371
+ ...(fallback ? { thinkingLevelMap: fallback } : {}),
372
+ }
298
373
  }
299
374
 
300
375
  function toPengepulModel(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pwguler/pi-pengepul-provider",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "pi custom provider for pengepul, a relay that pools your Claude/Codex subscriptions. Key and relay base live in auth.json; both native wires (Anthropic Messages, OpenAI Chat Completions) come from one catalog.",
6
6
  "license": "MIT",