@pwguler/pi-pengepul-provider 0.3.0 → 0.5.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 +33 -8
- package/extensions/config.ts +10 -27
- package/extensions/credential.ts +33 -57
- package/extensions/index.ts +6 -28
- package/extensions/models.ts +87 -111
- package/extensions/provider.ts +39 -69
- package/package.json +1 -1
- package/extensions/api-key.ts +0 -70
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
|
|
@@ -84,25 +89,45 @@ To reach a relay on another machine, put that machine's address in `baseUrl`
|
|
|
84
89
|
`~/.pengepul/config.yaml` on the relay, or forward the port over SSH). The key
|
|
85
90
|
is the relay's own key — `pengepul config api-key` prints it.
|
|
86
91
|
|
|
87
|
-
Optional overrides, for CI or a one-off shell.
|
|
88
|
-
credential
|
|
92
|
+
Optional overrides, for CI or a one-off shell. The first two outrank the
|
|
93
|
+
credential: set either and it wins, whatever `auth.json` holds.
|
|
89
94
|
|
|
90
95
|
| Setting | Env var | Default |
|
|
91
96
|
|---|---|---|
|
|
92
|
-
| Relay base URL | `PENGEPUL_BASE_URL` | `http://127.0.0.1:8317`, or the
|
|
93
|
-
| API key | `PENGEPUL_API_KEY` |
|
|
94
|
-
| Config path | `PENGEPUL_CONFIG` | `~/.pengepul/config.yaml` |
|
|
95
|
-
| Legacy cache path | `PENGEPUL_MODELS_CACHE` | `<agent-dir>/pengepul-models.json` |
|
|
97
|
+
| Relay base URL | `PENGEPUL_BASE_URL` | `http://127.0.0.1:8317`, or the credential's `baseUrl` |
|
|
98
|
+
| API key | `PENGEPUL_API_KEY` | the credential's `key` |
|
|
96
99
|
| Discovery timeout | `PENGEPUL_MODELS_TIMEOUT_MS` | `10000` |
|
|
97
100
|
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
```sh
|
|
102
|
+
PENGEPUL_BASE_URL=http://10.0.0.9:8317 PENGEPUL_API_KEY=sk-local-... pi
|
|
103
|
+
```
|
|
100
104
|
|
|
101
105
|
Do not set `providers.pengepul.baseUrl` in `models.json`. pi applies that value
|
|
102
106
|
to every model of the provider, which collapses the two wires onto one URL —
|
|
103
107
|
Anthropic Messages traffic would be sent to `/v1` and Chat Completions traffic
|
|
104
108
|
to `/`.
|
|
105
109
|
|
|
110
|
+
## Upgrading from 0.4.0 or earlier
|
|
111
|
+
|
|
112
|
+
This release is breaking: the relay's own `~/.pengepul/config.yaml` is no longer
|
|
113
|
+
a source. That file belongs to the machine running the relay, and reading it let
|
|
114
|
+
a key on that box configure every client sharing it.
|
|
115
|
+
|
|
116
|
+
- A key that came from `api-keys:` in it is no longer picked up. Run
|
|
117
|
+
`/login pengepul`, or put it in `auth.json` as `"pengepul".key`, or export
|
|
118
|
+
`PENGEPUL_API_KEY`. `pengepul config api-key` prints it.
|
|
119
|
+
- The relay base built from its `host` and `port` is no longer picked up
|
|
120
|
+
either. Set `baseUrl` in `auth.json`, or `PENGEPUL_BASE_URL`, if the relay is
|
|
121
|
+
not on `http://127.0.0.1:8317`.
|
|
122
|
+
- A pre-0.3 `<agent-dir>/pengepul-models.json` is no longer imported. The
|
|
123
|
+
catalog now comes from the relay on the next start, so the relay has to be
|
|
124
|
+
reachable once; pi's model store covers the rest.
|
|
125
|
+
|
|
126
|
+
Resolution order changed as well, and that is the part to check after an
|
|
127
|
+
upgrade: `PENGEPUL_BASE_URL` and `PENGEPUL_API_KEY` now win over `auth.json`,
|
|
128
|
+
where they previously lost to it. An exported key from an old shell now
|
|
129
|
+
overrides the one in the credential.
|
|
130
|
+
|
|
106
131
|
## Notes
|
|
107
132
|
|
|
108
133
|
- pengepul >= 0.6.0 advertises per-model context windows, output caps,
|
package/extensions/config.ts
CHANGED
|
@@ -1,34 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The environment overrides this provider reads.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* `~/.pi/agent/auth.json` is the configuration surface: the pengepul entry
|
|
5
|
+
* carries the relay API key and the relay base it applies to, and
|
|
6
|
+
* `/login pengepul` writes both. These variables outrank it, so a shell or a CI
|
|
7
|
+
* run can re-point the provider without editing the file.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import { CONFIG_PATH_ENV, DEFAULT_CONFIG_PATH } from "./api-key.ts"
|
|
12
|
-
|
|
13
|
-
export { CONFIG_PATH_ENV, DEFAULT_CONFIG_PATH }
|
|
14
|
-
|
|
10
|
+
/** Relay base URL. Outranks the credential's `baseUrl`. */
|
|
15
11
|
export const RELAY_BASE_ENV = "PENGEPUL_BASE_URL"
|
|
16
|
-
export const MODELS_CACHE_ENV = "PENGEPUL_MODELS_CACHE"
|
|
17
|
-
export const MODELS_TIMEOUT_MS_ENV = "PENGEPUL_MODELS_TIMEOUT_MS"
|
|
18
12
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
configPath: string
|
|
22
|
-
/** Where the pre-0.3 catalog cache lives; read once to seed pi's store. */
|
|
23
|
-
legacyCachePath: string
|
|
24
|
-
}
|
|
13
|
+
/** Relay API key. Outranks the credential's `key`. */
|
|
14
|
+
export const API_KEY_ENV = "PENGEPUL_API_KEY"
|
|
25
15
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
agentDir: string,
|
|
29
|
-
): PengepulSettings {
|
|
30
|
-
return {
|
|
31
|
-
configPath: env[CONFIG_PATH_ENV] ?? DEFAULT_CONFIG_PATH,
|
|
32
|
-
legacyCachePath: env[MODELS_CACHE_ENV] ?? join(agentDir, "pengepul-models.json"),
|
|
33
|
-
}
|
|
34
|
-
}
|
|
16
|
+
/** Milliseconds before model discovery gives up. */
|
|
17
|
+
export const MODELS_TIMEOUT_MS_ENV = "PENGEPUL_MODELS_TIMEOUT_MS"
|
package/extensions/credential.ts
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
* The credential is the pengepul entry in pi's `auth.json`: an API key for the
|
|
5
5
|
* relay plus the relay base it applies to. pi core resolves the key itself but
|
|
6
6
|
* knows nothing about `baseUrl`, so the extension reads it here. Everything in
|
|
7
|
-
* this module is pure: an env map and
|
|
7
|
+
* this module is pure: an env map and credential fields in, a value out.
|
|
8
8
|
*
|
|
9
|
-
* Precedence
|
|
10
|
-
*
|
|
11
|
-
* loopback default pengepul binds to.
|
|
9
|
+
* Precedence is the same for both, highest first: the `PENGEPUL_*` environment
|
|
10
|
+
* override, then the credential, then the loopback default pengepul binds to.
|
|
12
11
|
*/
|
|
13
12
|
|
|
13
|
+
import { API_KEY_ENV } from "./config.ts"
|
|
14
14
|
import { normalizeRootBaseUrl } from "./dialect.ts"
|
|
15
15
|
import { DEFAULT_RELAY_BASE } from "./models.ts"
|
|
16
16
|
|
|
@@ -38,76 +38,52 @@ export function credentialApiKey(credential: PengepulCredential | undefined): st
|
|
|
38
38
|
return nonEmptyString(credential?.key)
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
* Build a relay base from pengepul's own `config.yaml`. Only the machine running
|
|
43
|
-
* the relay has this file; clients reach a relay elsewhere and skip this source.
|
|
44
|
-
*
|
|
45
|
-
* pengepul writes `host: ''` when it binds loopback ("empty binds 127.0.0.1, not
|
|
46
|
-
* every interface"), so an empty host means loopback rather than "unset". A
|
|
47
|
-
* bind-all address is not a connect address either: `0.0.0.0`, `::`, and `*` mean
|
|
48
|
-
* "every interface", and a client reaches that listener on loopback.
|
|
49
|
-
*/
|
|
50
|
-
export function relayBaseFromConfigText(configText: string | undefined): string | undefined {
|
|
51
|
-
if (configText === undefined) return undefined
|
|
52
|
-
|
|
53
|
-
let host = "127.0.0.1"
|
|
54
|
-
let port: number | undefined
|
|
55
|
-
|
|
56
|
-
for (const line of configText.split(/\r?\n/)) {
|
|
57
|
-
const hostMatch = /^\s*host:\s*(.*)$/.exec(line)
|
|
58
|
-
if (hostMatch) {
|
|
59
|
-
const value = (hostMatch[1] ?? "").trim().replace(/^["']|["']$/g, "")
|
|
60
|
-
if (value !== "" && !isBindAll(value)) host = value
|
|
61
|
-
continue
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const portMatch = /^\s*port:\s*(.*)$/.exec(line)
|
|
65
|
-
if (portMatch) {
|
|
66
|
-
const value = (portMatch[1] ?? "").trim().replace(/^["']|["']$/g, "")
|
|
67
|
-
const parsed = Number(value)
|
|
68
|
-
if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65_535) port = parsed
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return port === undefined ? undefined : `http://${host}:${port}`
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Whether a configured host is a bind-all address rather than a destination. */
|
|
76
|
-
function isBindAll(host: string): boolean {
|
|
77
|
-
return host === "0.0.0.0" || host === "::" || host === "[::]" || host === "*"
|
|
78
|
-
}
|
|
79
|
-
|
|
80
41
|
export interface RelayBaseSources {
|
|
81
|
-
/** The relay base stored on the pengepul credential. */
|
|
82
|
-
credential?: string | undefined
|
|
83
42
|
/** The `PENGEPUL_BASE_URL` environment value. */
|
|
84
43
|
environment?: string | undefined
|
|
85
|
-
/** The relay base
|
|
86
|
-
|
|
44
|
+
/** The relay base stored on the pengepul credential. */
|
|
45
|
+
credential?: string | undefined
|
|
87
46
|
}
|
|
88
47
|
|
|
89
48
|
/**
|
|
90
|
-
* The relay root every wire appends to:
|
|
91
|
-
* loopback. A trailing `/v1` is accepted and stripped, so a
|
|
92
|
-
* client config cannot produce `/v1/v1` downstream.
|
|
49
|
+
* The relay root every wire appends to: the environment override, else the
|
|
50
|
+
* credential, else loopback. A trailing `/v1` is accepted and stripped, so a
|
|
51
|
+
* base copied from a client config cannot produce `/v1/v1` downstream.
|
|
93
52
|
*/
|
|
94
53
|
export function resolveRelayBase(sources: RelayBaseSources): string {
|
|
95
54
|
const chosen =
|
|
96
|
-
nonEmptyString(sources.credential) ??
|
|
97
55
|
nonEmptyString(sources.environment) ??
|
|
98
|
-
nonEmptyString(sources.
|
|
56
|
+
nonEmptyString(sources.credential) ??
|
|
99
57
|
DEFAULT_RELAY_BASE
|
|
100
58
|
return normalizeRootBaseUrl(chosen)
|
|
101
59
|
}
|
|
102
60
|
|
|
103
61
|
export interface ApiKeySources {
|
|
62
|
+
/** The `PENGEPUL_API_KEY` environment value. */
|
|
63
|
+
environment?: string | undefined
|
|
104
64
|
/** The key stored on the pengepul credential. */
|
|
105
65
|
credential?: string | undefined
|
|
106
|
-
/** The key resolved from `PENGEPUL_API_KEY` or pengepul's own config file. */
|
|
107
|
-
ambient?: string | undefined
|
|
108
66
|
}
|
|
109
67
|
|
|
110
|
-
/**
|
|
111
|
-
export
|
|
112
|
-
|
|
68
|
+
/** Which source answered, as the auth status labels it. */
|
|
69
|
+
export type ApiKeySource = "PENGEPUL_API_KEY" | "stored credential"
|
|
70
|
+
|
|
71
|
+
export interface ResolvedApiKey {
|
|
72
|
+
key: string
|
|
73
|
+
source: ApiKeySource
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The key requests and catalog fetches use, or undefined when none is
|
|
78
|
+
* configured. The value and the label it reports come from one walk, so the
|
|
79
|
+
* status UI cannot name a source the request did not use.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveApiKey(sources: ApiKeySources): ResolvedApiKey | undefined {
|
|
82
|
+
const environment = nonEmptyString(sources.environment)
|
|
83
|
+
if (environment) return { key: environment, source: API_KEY_ENV }
|
|
84
|
+
|
|
85
|
+
const credential = nonEmptyString(sources.credential)
|
|
86
|
+
if (credential) return { key: credential, source: "stored credential" }
|
|
87
|
+
|
|
88
|
+
return undefined
|
|
113
89
|
}
|
package/extensions/index.ts
CHANGED
|
@@ -4,36 +4,18 @@
|
|
|
4
4
|
* Registers pengepul as a pi provider. pengepul is a relay that pools your
|
|
5
5
|
* Claude/Codex subscriptions and speaks both native wires. The provider itself
|
|
6
6
|
* lives in `./provider.ts`: pi resolves auth through it and hands the credential
|
|
7
|
-
* back on every refresh, so `auth.json` carries the key and the relay base
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* back on every refresh, so `auth.json` carries the key and the relay base, and
|
|
8
|
+
* the `PENGEPUL_*` environment variables override both. The pure core is
|
|
9
|
+
* `./credential.ts`, `./dialect.ts`, and `./models.ts`; this file adapts them to
|
|
10
|
+
* the pi ExtensionAPI seam and reads nothing of its own.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
import {
|
|
13
|
-
getAgentDir,
|
|
14
|
-
type ExtensionAPI,
|
|
15
|
-
} from "@earendil-works/pi-coding-agent"
|
|
13
|
+
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
16
14
|
import { getBuiltinModel, getBuiltinModels, getBuiltinProviders } from "@earendil-works/pi-ai/providers/all"
|
|
17
|
-
import { readFileSync } from "node:fs"
|
|
18
15
|
|
|
19
|
-
import { resolveSettings } from "./config.ts"
|
|
20
16
|
import { catalogIdForms, type PengepulModel } from "./models.ts"
|
|
21
17
|
import { createPengepulProvider } from "./provider.ts"
|
|
22
18
|
|
|
23
|
-
function expandHome(path: string): string {
|
|
24
|
-
if (path === "~") return process.env.HOME ?? path
|
|
25
|
-
if (path.startsWith("~/")) return `${process.env.HOME ?? ""}${path.slice(1)}`
|
|
26
|
-
return path
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function readConfigText(path: string): string | undefined {
|
|
30
|
-
try {
|
|
31
|
-
return readFileSync(expandHome(path), "utf-8")
|
|
32
|
-
} catch {
|
|
33
|
-
return undefined
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
19
|
/** The metadata fields the lookup extracts from a pi catalog entry. */
|
|
38
20
|
function metaFromModel(model: NonNullable<ReturnType<typeof getBuiltinModel>>) {
|
|
39
21
|
return {
|
|
@@ -88,17 +70,13 @@ function createBuiltinLookup(): (id: string, dialect: string) => ReturnType<type
|
|
|
88
70
|
|
|
89
71
|
/**
|
|
90
72
|
* Model discovery belongs to pi: it calls `refreshModels` with the resolved
|
|
91
|
-
* credential, first against pi's
|
|
73
|
+
* credential, first against pi's model store and then, when the network is
|
|
92
74
|
* allowed, against the relay. Registration is synchronous; nothing here waits
|
|
93
75
|
* on the relay, and the catalog survives a restart through pi's model store.
|
|
94
76
|
*/
|
|
95
77
|
export default function (pi: ExtensionAPI) {
|
|
96
|
-
const settings = resolveSettings(process.env, getAgentDir())
|
|
97
|
-
|
|
98
78
|
const provider = createPengepulProvider({
|
|
99
79
|
env: process.env,
|
|
100
|
-
configText: readConfigText(settings.configPath),
|
|
101
|
-
legacyCachePath: settings.legacyCachePath,
|
|
102
80
|
lookupBuiltin: createBuiltinLookup(),
|
|
103
81
|
})
|
|
104
82
|
|
package/extensions/models.ts
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* pengepul model discovery.
|
|
3
3
|
*
|
|
4
4
|
* Fetches the relay's model catalog (`GET /v1/models`) and maps it into the
|
|
5
|
-
* pi-ai `ProviderModelConfig` shape
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* the next successful
|
|
5
|
+
* pi-ai `ProviderModelConfig` shape. Discovery is pi's to drive: the catalog
|
|
6
|
+
* lives in pi's model store between runs, so a briefly absent relay is covered
|
|
7
|
+
* by the store the provider registered rather than by a file this module owns.
|
|
8
|
+
* An empty result leaves pengepul models unavailable until the next successful
|
|
9
|
+
* startup refresh.
|
|
9
10
|
*
|
|
10
11
|
* The relay advertises id/owned_by and, since pengepul 0.6.0, optional
|
|
11
12
|
* per-model metadata: `context_window`, `max_output_tokens`,
|
|
@@ -17,11 +18,10 @@
|
|
|
17
18
|
* next best source - and then family heuristics. The catalog lookup is
|
|
18
19
|
* injected, so this module imports nothing from pi-ai and tests pin it.
|
|
19
20
|
*
|
|
20
|
-
* The
|
|
21
|
+
* The catalog fetch is injected as well, so this module holds no hidden
|
|
22
|
+
* network access and tests pin the transport.
|
|
21
23
|
*/
|
|
22
24
|
|
|
23
|
-
import { readFile } from "node:fs/promises"
|
|
24
|
-
|
|
25
25
|
import { MODELS_TIMEOUT_MS_ENV } from "./config.ts"
|
|
26
26
|
import { baseUrlForDialect, dialectForModelId } from "./dialect.ts"
|
|
27
27
|
import type { PengepulDialect } from "./dialect.ts"
|
|
@@ -32,8 +32,6 @@ export const DEFAULT_MODELS_TIMEOUT_MS = 10_000
|
|
|
32
32
|
const DEFAULT_CONTEXT_WINDOW = 200_000
|
|
33
33
|
const DEFAULT_MAX_TOKENS = 64_000
|
|
34
34
|
const ZERO_COST: ModelCostRates = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
|
35
|
-
/** v5 cached entries predate the relay-uniform off/minimal overlay (foreign catalog maps offered levels the relay 400s); reject it. */
|
|
36
|
-
const MODEL_CACHE_VERSION = 6
|
|
37
35
|
|
|
38
36
|
export type ModelInput = ("text" | "image")[]
|
|
39
37
|
|
|
@@ -259,6 +257,63 @@ function fallbackLevelMap(id: string): Record<string, string | null> | undefined
|
|
|
259
257
|
return id.toLowerCase().startsWith("commandcode/") ? { max: "max" } : undefined
|
|
260
258
|
}
|
|
261
259
|
|
|
260
|
+
/** The levels pi hides unless a map names them. */
|
|
261
|
+
const INHERITED_LEVELS = ["xhigh", "max"] as const
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Extended levels a family's previous minor already carries.
|
|
265
|
+
*
|
|
266
|
+
* pi hides `xhigh` and `max` unless a model's map names them, so an id the
|
|
267
|
+
* catalogs have not caught up with - a Claude point release the relay serves
|
|
268
|
+
* today, before pi's catalog carries it - dropped to low/medium/high and could
|
|
269
|
+
* never send the top of a ladder its own family publishes: in the catalog Opus
|
|
270
|
+
* 4.5 names neither extension, 4.6 names max, and 4.7, 4.8 and 5 name xhigh and
|
|
271
|
+
* max. The previous minor's own entry is that answer, and the only evidence
|
|
272
|
+
* there is: the relay advertises no effort metadata, so `GET /v1/models` has
|
|
273
|
+
* nothing to say about levels and pi's map is the whole vocabulary.
|
|
274
|
+
*
|
|
275
|
+
* Add-only by construction. Only levels the sibling maps to a string come
|
|
276
|
+
* across, so an id that reaches this path can gain the top of the scale but
|
|
277
|
+
* never lose a level that works today, and never takes on a sibling's spelling
|
|
278
|
+
* for off/low/medium/high - the dialect rules downstream own those.
|
|
279
|
+
*
|
|
280
|
+
* Measured against the live catalog at the time of writing: exactly two ids
|
|
281
|
+
* enter this path, `anthropic/claude-opus-5-5` and
|
|
282
|
+
* `openrouter/anthropic/claude-opus-5.5`, each inheriting xhigh and max from
|
|
283
|
+
* `claude-opus-5`. No other family in that catalog was touched.
|
|
284
|
+
*/
|
|
285
|
+
function inheritedLevelMap(
|
|
286
|
+
id: string,
|
|
287
|
+
dialect: PengepulDialect,
|
|
288
|
+
lookup: BuiltinModelLookup | undefined,
|
|
289
|
+
): Record<string, string | null> | undefined {
|
|
290
|
+
const siblingId = previousMinorId(id)
|
|
291
|
+
const siblingMap = siblingId ? lookup?.(siblingId, dialect)?.thinkingLevelMap : undefined
|
|
292
|
+
if (!siblingMap) return undefined
|
|
293
|
+
|
|
294
|
+
const inherited: Record<string, string | null> = {}
|
|
295
|
+
for (const level of INHERITED_LEVELS) {
|
|
296
|
+
const mapped = siblingMap[level]
|
|
297
|
+
if (typeof mapped === "string") inherited[level] = mapped
|
|
298
|
+
}
|
|
299
|
+
return Object.keys(inherited).length > 0 ? inherited : undefined
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The same id one minor older, keeping the routing namespace:
|
|
304
|
+
* `anthropic/claude-opus-5-5` -> `anthropic/claude-opus-5`, and the dotted
|
|
305
|
+
* `claude-opus-5.5` -> `claude-opus-5` for the Chat Completions spelling.
|
|
306
|
+
* Undefined when the name carries no trailing version segment to drop, which
|
|
307
|
+
* keeps qualifiers (`-preview`, `-flash`, `-fast`) out of the derivation.
|
|
308
|
+
*/
|
|
309
|
+
function previousMinorId(id: string): string | undefined {
|
|
310
|
+
const slash = id.lastIndexOf("/")
|
|
311
|
+
const name = slash === -1 ? id : id.slice(slash + 1)
|
|
312
|
+
const stripped = name.replace(/[.-]\d+$/, "")
|
|
313
|
+
if (stripped === name || stripped === "") return undefined
|
|
314
|
+
return slash === -1 ? stripped : `${id.slice(0, slash + 1)}${stripped}`
|
|
315
|
+
}
|
|
316
|
+
|
|
262
317
|
/**
|
|
263
318
|
* Resolve a model's metadata, most trustworthy source first:
|
|
264
319
|
* 1. what pengepul advertises (first-party for this relay),
|
|
@@ -266,6 +321,11 @@ function fallbackLevelMap(id: string): Record<string, string | null> | undefined
|
|
|
266
321
|
* 3. family heuristics.
|
|
267
322
|
* Sources merge per field, so a relay that sends only `context_window` still
|
|
268
323
|
* picks up pricing and modalities from the catalog.
|
|
324
|
+
*
|
|
325
|
+
* A catalog miss on a reasoning model still gets its extended levels from the
|
|
326
|
+
* family's previous minor where the catalog carries one (`inheritedLevelMap`);
|
|
327
|
+
* the namespace fallback for the Chat Completions wire then takes precedence
|
|
328
|
+
* over both.
|
|
269
329
|
*/
|
|
270
330
|
function metaFor(
|
|
271
331
|
entry: Record<string, unknown>,
|
|
@@ -294,7 +354,20 @@ function metaFor(
|
|
|
294
354
|
const fallback =
|
|
295
355
|
known || !reasoning || dialect !== "openai-completions" ? undefined : fallbackLevelMap(id)
|
|
296
356
|
|
|
297
|
-
|
|
357
|
+
// A catalog miss is not always silence: the family's previous minor answers
|
|
358
|
+
// for the levels the catalog has not named yet. Whatever the base resolved
|
|
359
|
+
// wins over it, so a relay that starts advertising a map still outranks this.
|
|
360
|
+
const inherited = known || !reasoning ? undefined : inheritedLevelMap(id, dialect, lookup)
|
|
361
|
+
const levelMap = inherited
|
|
362
|
+
? { ...inherited, ...(meta.thinkingLevelMap ?? {}) }
|
|
363
|
+
: meta.thinkingLevelMap
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
...meta,
|
|
367
|
+
reasoning,
|
|
368
|
+
...(levelMap ? { thinkingLevelMap: levelMap } : {}),
|
|
369
|
+
...(fallback ? { thinkingLevelMap: fallback } : {}),
|
|
370
|
+
}
|
|
298
371
|
}
|
|
299
372
|
|
|
300
373
|
function toPengepulModel(
|
|
@@ -416,10 +489,10 @@ function isBatchRoute(id: string): boolean {
|
|
|
416
489
|
}
|
|
417
490
|
|
|
418
491
|
/**
|
|
419
|
-
* Drop the ids this relay cannot serve on either wire
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
492
|
+
* Drop the ids this relay cannot serve on either wire, on the way in from the
|
|
493
|
+
* relay. The store replays what pi persisted, so a route this filter drops
|
|
494
|
+
* survives in a store written before the filter existed only until the next
|
|
495
|
+
* successful fetch replaces it.
|
|
423
496
|
*/
|
|
424
497
|
function servableModels(models: readonly PengepulModel[]): PengepulModel[] {
|
|
425
498
|
return models.filter((model) => !isBatchRoute(model.id))
|
|
@@ -617,11 +690,6 @@ interface FetchModelsOptions {
|
|
|
617
690
|
lookupBuiltin?: BuiltinModelLookup
|
|
618
691
|
}
|
|
619
692
|
|
|
620
|
-
interface LoadModelsOptions extends FetchModelsOptions {
|
|
621
|
-
cachePath: string
|
|
622
|
-
relayBase: string
|
|
623
|
-
}
|
|
624
|
-
|
|
625
693
|
function errorMessage(error: unknown): string {
|
|
626
694
|
return error instanceof Error ? error.message : String(error)
|
|
627
695
|
}
|
|
@@ -741,95 +809,3 @@ export async function fetchPengepulModels(
|
|
|
741
809
|
|
|
742
810
|
return modelsFromApiResponse(body, options.lookupBuiltin)
|
|
743
811
|
}
|
|
744
|
-
|
|
745
|
-
function numberField(record: Record<string, unknown>, key: string): number {
|
|
746
|
-
const value = record[key]
|
|
747
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
748
|
-
throw new Error(`Expected ${key} to be a finite number`)
|
|
749
|
-
}
|
|
750
|
-
return value
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
function inputField(record: Record<string, unknown>, key: string): ModelInput {
|
|
754
|
-
const value = record[key]
|
|
755
|
-
if (!Array.isArray(value)) throw new Error(`Expected ${key} to be an array`)
|
|
756
|
-
return value.map((entry) => {
|
|
757
|
-
if (entry !== "text" && entry !== "image") {
|
|
758
|
-
throw new Error(`Expected ${key} entries to be "text" or "image"`)
|
|
759
|
-
}
|
|
760
|
-
return entry
|
|
761
|
-
})
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
function costField(record: Record<string, unknown>, key: string): ModelCostRates {
|
|
765
|
-
const value = record[key]
|
|
766
|
-
if (!isRecord(value)) throw new Error(`Expected ${key} to be an object`)
|
|
767
|
-
return {
|
|
768
|
-
input: numberField(value, "input"),
|
|
769
|
-
output: numberField(value, "output"),
|
|
770
|
-
cacheRead: numberField(value, "cacheRead"),
|
|
771
|
-
cacheWrite: numberField(value, "cacheWrite"),
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
function levelMapField(
|
|
776
|
-
record: Record<string, unknown>,
|
|
777
|
-
key: string,
|
|
778
|
-
): Record<string, string | null> {
|
|
779
|
-
const value = record[key]
|
|
780
|
-
if (!isRecord(value)) throw new Error(`Expected ${key} to be an object`)
|
|
781
|
-
const map: Record<string, string | null> = {}
|
|
782
|
-
for (const [level, mapped] of Object.entries(value)) {
|
|
783
|
-
if (mapped !== null && typeof mapped !== "string") {
|
|
784
|
-
throw new Error(`Expected ${key} values to be strings or null`)
|
|
785
|
-
}
|
|
786
|
-
map[level] = mapped
|
|
787
|
-
}
|
|
788
|
-
return map
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
export function modelsFromCache(value: unknown): readonly PengepulModel[] {
|
|
792
|
-
if (!isRecord(value)) throw new Error("Expected model cache to be an object")
|
|
793
|
-
if (value["version"] !== MODEL_CACHE_VERSION) {
|
|
794
|
-
throw new Error(`Expected model cache version ${MODEL_CACHE_VERSION}`)
|
|
795
|
-
}
|
|
796
|
-
if (!Array.isArray(value["models"])) throw new Error("Expected cached models to be an array")
|
|
797
|
-
|
|
798
|
-
const parsed: PengepulModel[] = value["models"].map((entry) => {
|
|
799
|
-
if (!isRecord(entry)) throw new Error("Expected cached model entry to be an object")
|
|
800
|
-
return {
|
|
801
|
-
id: stringField(entry, "id"),
|
|
802
|
-
name: stringField(entry, "name"),
|
|
803
|
-
dialect: stringField(entry, "dialect") as PengepulDialect,
|
|
804
|
-
reasoning: entry["reasoning"] === true,
|
|
805
|
-
input: inputField(entry, "input"),
|
|
806
|
-
cost: costField(entry, "cost"),
|
|
807
|
-
contextWindow: numberField(entry, "contextWindow"),
|
|
808
|
-
maxTokens: numberField(entry, "maxTokens"),
|
|
809
|
-
...(entry["thinkingLevelMap"] !== undefined
|
|
810
|
-
? { thinkingLevelMap: levelMapField(entry, "thinkingLevelMap") }
|
|
811
|
-
: {}),
|
|
812
|
-
}
|
|
813
|
-
})
|
|
814
|
-
const servable = servableModels(parsed)
|
|
815
|
-
if (servable.length === 0) throw new Error("pengepul cache holds no valid models")
|
|
816
|
-
return servable
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
async function readCache(cachePath: string): Promise<readonly PengepulModel[]> {
|
|
820
|
-
const contents = await readFile(cachePath, "utf-8")
|
|
821
|
-
return modelsFromCache(JSON.parse(contents))
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
/** Reads the cached catalog without touching the network; empty when missing/invalid. */
|
|
825
|
-
export async function loadCachedPengepulModels(
|
|
826
|
-
cachePath: string,
|
|
827
|
-
): Promise<readonly PengepulModel[]> {
|
|
828
|
-
try {
|
|
829
|
-
return await readCache(cachePath)
|
|
830
|
-
} catch {
|
|
831
|
-
return []
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
|
package/extensions/provider.ts
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* The pengepul provider, as pi-ai sees it.
|
|
3
3
|
*
|
|
4
4
|
* Registered as a native provider, so pi resolves auth through this module and
|
|
5
|
-
* hands the credential back to `refreshModels`.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* hands the credential back to `refreshModels`. Discovery and requests
|
|
6
|
+
* therefore resolve the same key and the same relay base: environment first,
|
|
7
|
+
* then the credential. A provider declared as plain config values has no
|
|
8
|
+
* credential at discovery time, so a key stored in `auth.json` left its
|
|
9
|
+
* catalog fetch with nothing to send but an environment variable, and a 401.
|
|
10
10
|
*
|
|
11
11
|
* The relay base lives on the credential as `baseUrl`, a field pi core does not
|
|
12
12
|
* read. Per-model base URLs are derived from it, because pi applies a base URL
|
|
@@ -27,12 +27,10 @@ import type {
|
|
|
27
27
|
} from "@earendil-works/pi-ai"
|
|
28
28
|
import { anthropicMessagesApi, lazyStream, openAICompletionsApi } from "@earendil-works/pi-ai/compat"
|
|
29
29
|
|
|
30
|
-
import { API_KEY_ENV,
|
|
31
|
-
import { RELAY_BASE_ENV } from "./config.ts"
|
|
30
|
+
import { API_KEY_ENV, RELAY_BASE_ENV } from "./config.ts"
|
|
32
31
|
import {
|
|
33
32
|
credentialApiKey,
|
|
34
33
|
credentialRelayBase,
|
|
35
|
-
relayBaseFromConfigText,
|
|
36
34
|
resolveApiKey,
|
|
37
35
|
resolveRelayBase,
|
|
38
36
|
type PengepulCredential,
|
|
@@ -43,7 +41,6 @@ import {
|
|
|
43
41
|
fetchPengepulModels,
|
|
44
42
|
getModelsTimeoutMs,
|
|
45
43
|
isPengepulModelEntry,
|
|
46
|
-
loadCachedPengepulModels,
|
|
47
44
|
PENGEPUL_PROVIDER_ID,
|
|
48
45
|
restampRelayBase,
|
|
49
46
|
toPengepulModels,
|
|
@@ -58,16 +55,9 @@ export interface PengepulApiKeyCredential extends ApiKeyCredential {
|
|
|
58
55
|
baseUrl?: string
|
|
59
56
|
}
|
|
60
57
|
|
|
61
|
-
/** Where the config-file fallback key comes from, for the status label. */
|
|
62
|
-
const CONFIG_FILE_LABEL = "~/.pengepul/config.yaml"
|
|
63
|
-
|
|
64
58
|
export interface PengepulProviderOptions {
|
|
65
59
|
/** Environment map holding the optional `PENGEPUL_*` overrides. */
|
|
66
60
|
env?: Record<string, string | undefined>
|
|
67
|
-
/** Text of pengepul's own config, when this machine happens to run the relay. */
|
|
68
|
-
configText?: string
|
|
69
|
-
/** Where the pre-0.3 catalog cache lives; read once to seed pi's store. */
|
|
70
|
-
legacyCachePath: string
|
|
71
61
|
/** Catalog fetch transport, for tests. */
|
|
72
62
|
fetchImpl?: typeof fetch
|
|
73
63
|
/** Builtin metadata lookup, injected so the catalog logic stays free of pi imports. */
|
|
@@ -90,9 +80,9 @@ function isPengepulDialect(api: Api): api is PengepulDialect {
|
|
|
90
80
|
/**
|
|
91
81
|
* A provider whose catalog comes from pengepul and whose auth is the relay key.
|
|
92
82
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
83
|
+
* The environment overrides are read from the captured environment, not from
|
|
84
|
+
* `AuthContext.env`, because `refreshModels` receives no auth context and both
|
|
85
|
+
* paths must agree on what the key and base are.
|
|
96
86
|
*/
|
|
97
87
|
export function createPengepulProvider(options: PengepulProviderOptions): Provider {
|
|
98
88
|
const env = options.env ?? process.env
|
|
@@ -104,23 +94,23 @@ export function createPengepulProvider(options: PengepulProviderOptions): Provid
|
|
|
104
94
|
|
|
105
95
|
const environmentRelayBase = env[RELAY_BASE_ENV]
|
|
106
96
|
const environmentApiKey = env[API_KEY_ENV]
|
|
107
|
-
const configText = options.configText
|
|
108
|
-
const configRelayBase = relayBaseFromConfigText(configText)
|
|
109
|
-
const configApiKey = configText ? extractApiKeys(configText)[0] : undefined
|
|
110
97
|
|
|
111
98
|
let models: PengepulModelEntry[] = []
|
|
112
99
|
|
|
113
|
-
/** The
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
100
|
+
/** The relay base in force: the environment override, else the credential, else loopback. */
|
|
101
|
+
function relayBaseFor(credential: unknown): string {
|
|
102
|
+
return resolveRelayBase({
|
|
103
|
+
environment: environmentRelayBase,
|
|
104
|
+
credential: credentialRelayBase(credential as PengepulCredential | undefined),
|
|
105
|
+
})
|
|
118
106
|
}
|
|
119
107
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
108
|
+
/** The key in force, with the label the status UI shows for it. */
|
|
109
|
+
function resolveKey(credential: unknown) {
|
|
110
|
+
return resolveApiKey({
|
|
111
|
+
environment: environmentApiKey,
|
|
112
|
+
credential: credentialApiKey(credential as PengepulCredential | undefined),
|
|
113
|
+
})
|
|
124
114
|
}
|
|
125
115
|
|
|
126
116
|
const streams: Record<PengepulDialect, ProviderStreams> = {
|
|
@@ -138,38 +128,26 @@ export function createPengepulProvider(options: PengepulProviderOptions): Provid
|
|
|
138
128
|
})
|
|
139
129
|
}
|
|
140
130
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Re-publish pi's stored catalog with every base URL re-derived from the
|
|
133
|
+
* base in force, so a relay that moved is not reached at its old address.
|
|
134
|
+
* Returns false only when pi refused the publication, the one case that
|
|
135
|
+
* skips the network phase.
|
|
136
|
+
*/
|
|
137
|
+
async function restoreStored(context: RefreshModelsContext): Promise<boolean> {
|
|
148
138
|
const stored = (context.stored?.models ?? []).filter(isPengepulModelEntry)
|
|
149
|
-
if (stored.length
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Pre-0.3 installs kept their own cache file. Read it once so an upgrade
|
|
155
|
-
// does not start blind; pi's store owns the catalog from here on.
|
|
156
|
-
const legacy = await loadCachedPengepulModels(options.legacyCachePath)
|
|
157
|
-
if (legacy.length === 0) return true
|
|
158
|
-
const seeded = toPengepulModels(legacy, relayBase)
|
|
159
|
-
return context.publish({ update: () => { models = seeded } })
|
|
139
|
+
if (stored.length === 0) return true
|
|
140
|
+
const restored = restampRelayBase(stored, relayBaseFor(context.credential))
|
|
141
|
+
return context.publish({ update: () => { models = restored } })
|
|
160
142
|
}
|
|
161
143
|
|
|
162
144
|
async function refreshFromRelay(context: RefreshModelsContext): Promise<void> {
|
|
163
|
-
const relayBase =
|
|
164
|
-
credential: credentialRelayBase(context.credential as PengepulCredential | undefined),
|
|
165
|
-
environment: environmentRelayBase,
|
|
166
|
-
config: configRelayBase,
|
|
167
|
-
})
|
|
145
|
+
const relayBase = relayBaseFor(context.credential)
|
|
168
146
|
|
|
169
147
|
const resolved = resolveKey(context.credential)
|
|
170
|
-
if (!resolved
|
|
148
|
+
if (!resolved) {
|
|
171
149
|
logWarning(
|
|
172
|
-
`No pengepul API key is configured (${API_KEY_ENV}
|
|
150
|
+
`No pengepul API key is configured (${API_KEY_ENV} or auth.json), so the model catalog cannot be refreshed. Run /login pengepul.`,
|
|
173
151
|
)
|
|
174
152
|
return
|
|
175
153
|
}
|
|
@@ -201,10 +179,7 @@ export function createPengepulProvider(options: PengepulProviderOptions): Provid
|
|
|
201
179
|
return {
|
|
202
180
|
id: PENGEPUL_PROVIDER_ID,
|
|
203
181
|
name: "Pengepul",
|
|
204
|
-
baseUrl: resolveRelayBase({
|
|
205
|
-
environment: environmentRelayBase,
|
|
206
|
-
config: configRelayBase,
|
|
207
|
-
}),
|
|
182
|
+
baseUrl: resolveRelayBase({ environment: environmentRelayBase }),
|
|
208
183
|
auth: {
|
|
209
184
|
apiKey: {
|
|
210
185
|
name: "Pengepul relay",
|
|
@@ -231,25 +206,20 @@ export function createPengepulProvider(options: PengepulProviderOptions): Provid
|
|
|
231
206
|
},
|
|
232
207
|
check: async (input): Promise<AuthCheck | undefined> => {
|
|
233
208
|
const resolved = resolveKey(input.credential)
|
|
234
|
-
return resolved.
|
|
235
|
-
? { type: "api_key", ...(resolved.source ? { source: resolved.source } : {}) }
|
|
236
|
-
: undefined
|
|
209
|
+
return resolved ? { type: "api_key", source: resolved.source } : undefined
|
|
237
210
|
},
|
|
238
211
|
resolve: async (input): Promise<AuthResult | undefined> => {
|
|
239
212
|
const resolved = resolveKey(input.credential)
|
|
240
|
-
if (!resolved
|
|
213
|
+
if (!resolved) return undefined
|
|
241
214
|
// No `baseUrl` here on purpose: pi would apply it to every model and
|
|
242
215
|
// collapse the two dialect base URLs into one.
|
|
243
|
-
return {
|
|
244
|
-
auth: { apiKey: resolved.key },
|
|
245
|
-
...(resolved.source ? { source: resolved.source } : {}),
|
|
246
|
-
}
|
|
216
|
+
return { auth: { apiKey: resolved.key }, source: resolved.source }
|
|
247
217
|
},
|
|
248
218
|
},
|
|
249
219
|
},
|
|
250
220
|
getModels: () => models,
|
|
251
221
|
refreshModels: async (context) => {
|
|
252
|
-
if (!(await
|
|
222
|
+
if (!(await restoreStored(context))) return
|
|
253
223
|
if (!context.allowNetwork || context.signal.aborted) return
|
|
254
224
|
await refreshFromRelay(context)
|
|
255
225
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pwguler/pi-pengepul-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
package/extensions/api-key.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read pengepul's own API key.
|
|
3
|
-
*
|
|
4
|
-
* pengepul authenticates every request with a static key from its config:
|
|
5
|
-
* `~/.pengepul/config.yaml`, under `api-keys:` (the first is generated on first
|
|
6
|
-
* run and is `sk-local-...`). Clients send it as `Authorization: Bearer <key>`
|
|
7
|
-
* or `x-api-key: <key>`.
|
|
8
|
-
*
|
|
9
|
-
* This is the fallback for the machine that runs the relay itself: a client box
|
|
10
|
-
* configures the key in `auth.json`, which pi resolves and hands to the
|
|
11
|
-
* provider. Only extraction lives here, so the file read stays with the caller
|
|
12
|
-
* and this module stays io-free.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
export const DEFAULT_CONFIG_PATH = "~/.pengepul/config.yaml"
|
|
16
|
-
|
|
17
|
-
export const API_KEY_ENV = "PENGEPUL_API_KEY"
|
|
18
|
-
export const CONFIG_PATH_ENV = "PENGEPUL_CONFIG"
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Extract `api-keys:` entries from pengepul's YAML config, without a YAML
|
|
22
|
-
* dependency. Handles both the inline-flow form and the block-sequence form:
|
|
23
|
-
*
|
|
24
|
-
* api-keys: [sk-local-a, sk-local-b]
|
|
25
|
-
* api-keys:
|
|
26
|
-
* - sk-local-a
|
|
27
|
-
* - sk-local-b
|
|
28
|
-
*/
|
|
29
|
-
export function extractApiKeys(configText: string): string[] {
|
|
30
|
-
const lines = configText.split(/\r?\n/)
|
|
31
|
-
const keys: string[] = []
|
|
32
|
-
|
|
33
|
-
for (let i = 0; i < lines.length; i++) {
|
|
34
|
-
const line = lines[i]
|
|
35
|
-
if (line === undefined) continue
|
|
36
|
-
|
|
37
|
-
const match = /^\s*api-keys:\s*(.*)$/.exec(line)
|
|
38
|
-
if (!match) continue
|
|
39
|
-
|
|
40
|
-
const rest = (match[1] ?? "").trim()
|
|
41
|
-
if (rest.startsWith("[")) {
|
|
42
|
-
// Inline flow sequence: [a, b, c]
|
|
43
|
-
for (const token of rest.slice(1, -1).split(",")) {
|
|
44
|
-
const key = token.trim().replace(/^["']|["']$/g, "")
|
|
45
|
-
if (key) keys.push(key)
|
|
46
|
-
}
|
|
47
|
-
} else if (rest === "" || rest === "|" || rest === ">") {
|
|
48
|
-
// Block sequence follows. Sequence entries may sit at any indent —
|
|
49
|
-
// pengepul itself writes them flush with the key (`- sk-local-…`) —
|
|
50
|
-
// so scan forward through blanks, comments, and `- ` entries and
|
|
51
|
-
// stop at the first line that starts another key.
|
|
52
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
53
|
-
const item = lines[j]
|
|
54
|
-
if (item === undefined) continue
|
|
55
|
-
const token = item.trim()
|
|
56
|
-
if (token === "" || token.startsWith("#")) continue
|
|
57
|
-
if (!token.startsWith("-")) break
|
|
58
|
-
const value = token.slice(1).trim().replace(/^["']|["']$/g, "")
|
|
59
|
-
if (value) keys.push(value)
|
|
60
|
-
}
|
|
61
|
-
} else {
|
|
62
|
-
// Single inline scalar: api-keys: sk-local-a
|
|
63
|
-
const value = rest.replace(/^["']|["']$/g, "")
|
|
64
|
-
if (value) keys.push(value)
|
|
65
|
-
}
|
|
66
|
-
break
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return keys
|
|
70
|
-
}
|