@pwguler/pi-pengepul-provider 0.2.1 → 0.2.2
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/extensions/models.ts +119 -29
- package/package.json +1 -1
package/extensions/models.ts
CHANGED
|
@@ -92,20 +92,45 @@ export function bareId(id: string): string {
|
|
|
92
92
|
return slash === -1 ? id : id.slice(slash + 1)
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/** `openrouter/openai/gpt-5.4:batch` -> `gpt-5.4:batch`: the model name without every routing prefix. */
|
|
96
|
+
export function modelName(id: string): string {
|
|
97
|
+
const slash = id.lastIndexOf("/")
|
|
98
|
+
return slash === -1 ? id : id.slice(slash + 1)
|
|
99
|
+
}
|
|
100
|
+
|
|
95
101
|
/**
|
|
96
102
|
* Fallback for models pi's catalog does not know. Family-shaped but
|
|
97
|
-
* conservative; the builtin lookup wins whenever it has the id. The
|
|
98
|
-
*
|
|
99
|
-
*
|
|
103
|
+
* conservative; the builtin lookup wins whenever it has the id. The relay
|
|
104
|
+
* prefixes ids with routing namespaces (`openrouter/openai/gpt-5.4:batch`),
|
|
105
|
+
* so the family is read from the final segment. The final branch treats
|
|
106
|
+
* unknown ids as non-reasoning, so an unrecognized id never gets reasoning
|
|
107
|
+
* params the upstream may reject.
|
|
100
108
|
*/
|
|
101
109
|
function heuristicMeta(id: string): BuiltinModelMeta {
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
110
|
+
const name = modelName(id).toLowerCase()
|
|
111
|
+
if (name.startsWith("claude-")) {
|
|
104
112
|
return { reasoning: true, contextWindow: 200_000, maxTokens: 64_000, input: ["text"], cost: ZERO_COST }
|
|
105
113
|
}
|
|
106
|
-
if (
|
|
114
|
+
if (
|
|
115
|
+
name.startsWith("codex-") ||
|
|
116
|
+
/^gpt-[5-9]/.test(name) ||
|
|
117
|
+
name.startsWith("gpt-oss") ||
|
|
118
|
+
/^o[1-9]/.test(name)
|
|
119
|
+
) {
|
|
107
120
|
return { reasoning: true, contextWindow: 272_000, maxTokens: 64_000, input: ["text"], cost: ZERO_COST }
|
|
108
121
|
}
|
|
122
|
+
// Families that name their reasoning: DeepSeek's R1 line and ids that spell
|
|
123
|
+
// it out (`sonar-reasoning-pro`). The relay's own numbers win when it sends
|
|
124
|
+
// them; these are placeholders for the ids it describes with nothing else.
|
|
125
|
+
if (name.startsWith("deepseek-r1") || name.includes("reasoning")) {
|
|
126
|
+
return {
|
|
127
|
+
reasoning: true,
|
|
128
|
+
contextWindow: DEFAULT_CONTEXT_WINDOW,
|
|
129
|
+
maxTokens: DEFAULT_MAX_TOKENS,
|
|
130
|
+
input: ["text"],
|
|
131
|
+
cost: ZERO_COST,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
109
134
|
return {
|
|
110
135
|
reasoning: false,
|
|
111
136
|
contextWindow: DEFAULT_CONTEXT_WINDOW,
|
|
@@ -217,20 +242,19 @@ function toPengepulModel(
|
|
|
217
242
|
const reasoning = meta.reasoning || ownedBy === "anthropic"
|
|
218
243
|
|
|
219
244
|
// The relay enforces reasoning_effort low|medium|high|xhigh|max at its
|
|
220
|
-
// request layer, uniformly across families: `minimal` 400s and its
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
245
|
+
// request layer, uniformly across families: `minimal` 400s and its thinking
|
|
246
|
+
// toggle never actually disables thinking. That holds no matter where the
|
|
247
|
+
// reasoning knowledge came from, so every openai-completions reasoning model
|
|
248
|
+
// gets the relay's shape: inherited strings case-folded to the enum (pi's
|
|
249
|
+
// catalogs spell Google efforts `HIGH` and Qwen's `default`), values that
|
|
250
|
+
// match under no casing hidden, and off/minimal always null. Overlay, never
|
|
251
|
+
// replace: inherited nulls keep their levels hidden. The Messages dialect
|
|
252
|
+
// needs none of this: it folds `minimal` into `low` and already nulls `off`
|
|
253
|
+
// via forceAdaptiveThinking.
|
|
254
|
+
const thinkingLevelMap = relaySafeLevelMap(
|
|
255
|
+
meta.thinkingLevelMap,
|
|
256
|
+
reasoning && dialect === "openai-completions",
|
|
257
|
+
)
|
|
234
258
|
|
|
235
259
|
return {
|
|
236
260
|
id,
|
|
@@ -245,6 +269,37 @@ function toPengepulModel(
|
|
|
245
269
|
}
|
|
246
270
|
}
|
|
247
271
|
|
|
272
|
+
/** The efforts the relay accepts; anything else is rejected at its request layer. */
|
|
273
|
+
const RELAY_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"])
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Shape an inherited level map for the relay's wire. With `enforce` (an
|
|
277
|
+
* openai-completions reasoning model) the relay's enum is the only vocabulary
|
|
278
|
+
* that reaches it: inherited strings are case-folded to the enum or hidden,
|
|
279
|
+
* and off/minimal are always hidden. Without it the map passes through.
|
|
280
|
+
*/
|
|
281
|
+
function relaySafeLevelMap(
|
|
282
|
+
inherited: Record<string, string | null> | undefined,
|
|
283
|
+
enforce: boolean,
|
|
284
|
+
): Record<string, string | null> | undefined {
|
|
285
|
+
if (!enforce) return inherited ? { ...inherited } : undefined
|
|
286
|
+
|
|
287
|
+
const map: Record<string, string | null> = {}
|
|
288
|
+
for (const [level, mapped] of Object.entries(inherited ?? {})) {
|
|
289
|
+
map[level] = typeof mapped === "string" ? relayEffort(mapped) : mapped
|
|
290
|
+
}
|
|
291
|
+
map["off"] = null
|
|
292
|
+
map["minimal"] = null
|
|
293
|
+
return map
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** The relay's effort enum, case-folded; null keeps the level out of the picker. */
|
|
297
|
+
function relayEffort(value: string): string | null {
|
|
298
|
+
if (RELAY_EFFORTS.has(value)) return value
|
|
299
|
+
const lower = value.toLowerCase()
|
|
300
|
+
return RELAY_EFFORTS.has(lower) ? lower : null
|
|
301
|
+
}
|
|
302
|
+
|
|
248
303
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
249
304
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
250
305
|
}
|
|
@@ -295,7 +350,12 @@ export function toProviderModelConfigs(
|
|
|
295
350
|
contextWindow: number
|
|
296
351
|
maxTokens: number
|
|
297
352
|
thinkingLevelMap?: Record<string, string | null>
|
|
298
|
-
compat?: {
|
|
353
|
+
compat?: {
|
|
354
|
+
forceAdaptiveThinking?: boolean
|
|
355
|
+
supportsLongCacheRetention?: boolean
|
|
356
|
+
sendSessionAffinityHeaders?: boolean
|
|
357
|
+
sessionAffinityFormat?: "openai" | "openai-nosession" | "openrouter"
|
|
358
|
+
}
|
|
299
359
|
}> {
|
|
300
360
|
return models.map((model) => {
|
|
301
361
|
const adaptive = model.dialect === "anthropic-messages" && model.reasoning
|
|
@@ -327,14 +387,44 @@ export function toProviderModelConfigs(
|
|
|
327
387
|
// unsupported so pi omits the thinking param entirely (server default
|
|
328
388
|
// = adaptive), and forceAdaptiveThinking routes an explicit level to
|
|
329
389
|
// {type:"adaptive"} + effort instead of budget_tokens.
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
390
|
+
// Both dialects pin both fields, and that is why `compat` is
|
|
391
|
+
// unconditional. The relay's prompt-cache affinity key resolves in this
|
|
392
|
+
// order: `x-claude-code-session-id`, `x-session-id`, the body's
|
|
393
|
+
// `prompt_cache_key`, then a hash of the cacheable request prefix
|
|
394
|
+
// (app.rs `conversation_key`). pi emits `x-session-id` only for the
|
|
395
|
+
// `openrouter` affinity format, and only when the send flag is set;
|
|
396
|
+
// the auto-detected defaults are wrong here in both cases
|
|
397
|
+
// (openai-completions picks `openai`: session_id + x-client-request-id +
|
|
398
|
+
// x-session-affinity; anthropic-messages picks nothing). The header is
|
|
399
|
+
// the cheaper and more explicit of the two signals and it outranks the
|
|
400
|
+
// body field, so pinning it keeps a session's account stable by the
|
|
401
|
+
// relay's first rule rather than its third. Losing the pin costs a
|
|
402
|
+
// session that migrates between pooled accounts its whole prefix: the
|
|
403
|
+
// upstream cache is per account.
|
|
404
|
+
//
|
|
405
|
+
// Measured, not assumed — `test/affinity-wire.test.ts` dumps both bodies:
|
|
406
|
+
// openai-completions carries `prompt_cache_key: <sessionId>` (and
|
|
407
|
+
// `prompt_cache_retention: "24h"`) under PI_CACHE_RETENTION=long, so the
|
|
408
|
+
// body field alone would name the conversation; anthropic-messages
|
|
409
|
+
// carries no `prompt_cache_key` at all, and pi-ai hardcodes
|
|
410
|
+
// `x-session-affinity` there, which this relay does not read. Messages
|
|
411
|
+
// traffic therefore rests entirely on the relay's prefix fallback until
|
|
412
|
+
// a pi release honours `sessionAffinityFormat` on that dialect.
|
|
413
|
+
//
|
|
414
|
+
// The two dialects do not land at the same time. openai-completions
|
|
415
|
+
// honors `sessionAffinityFormat` in every released pi. anthropic-messages
|
|
416
|
+
// only reads it from the unreleased change on pi main (commit
|
|
417
|
+
// bbb61e34a); through pi-ai 0.85.1 that client hardcodes the header name
|
|
418
|
+
// `x-session-affinity`, which this relay does not read, so the Claude
|
|
419
|
+
// pin below is inert until pi ships it. Pin now rather than later: the
|
|
420
|
+
// cost of an ignored header is zero, and the cost of forgetting is
|
|
421
|
+
// silently re-billed Claude prefixes.
|
|
422
|
+
compat: {
|
|
423
|
+
...(adaptive ? { forceAdaptiveThinking: true as const } : {}),
|
|
424
|
+
...(longCacheRetention ? { supportsLongCacheRetention: true as const } : {}),
|
|
425
|
+
sendSessionAffinityHeaders: true as const,
|
|
426
|
+
sessionAffinityFormat: "openrouter" as const,
|
|
427
|
+
},
|
|
338
428
|
}
|
|
339
429
|
})
|
|
340
430
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pwguler/pi-pengepul-provider",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "pi custom provider for pengepul, a local relay that pools your Claude/Codex subscriptions. Connects pi to http://127.0.0.1:8317 over the native Anthropic Messages and OpenAI Chat Completions wires.",
|
|
6
6
|
"license": "MIT",
|