@pwguler/pi-pengepul-provider 0.2.3 → 0.3.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 +67 -12
- package/extensions/api-key.ts +6 -37
- package/extensions/config.ts +15 -21
- package/extensions/credential.ts +113 -0
- package/extensions/index.ts +37 -74
- package/extensions/models.ts +310 -198
- package/extensions/provider.ts +269 -0
- package/package.json +2 -2
- package/extensions/runtime.ts +0 -211
package/README.md
CHANGED
|
@@ -41,8 +41,8 @@ prefixed `pengepul/<id>`. To try it without installing, use
|
|
|
41
41
|
|
|
42
42
|
## What it does
|
|
43
43
|
|
|
44
|
-
- Registers the `pengepul` provider against
|
|
45
|
-
(`http://127.0.0.1:8317` by default).
|
|
44
|
+
- Registers the `pengepul` provider against the relay base URL on your
|
|
45
|
+
credential (`http://127.0.0.1:8317` by default).
|
|
46
46
|
- Discovers models from `GET /v1/models`, maps each to the right wire:
|
|
47
47
|
- `claude-*` / `anthropic/*` and `owned_by: anthropic` → Anthropic Messages
|
|
48
48
|
(`POST /v1/messages`),
|
|
@@ -53,24 +53,55 @@ 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
|
-
- Caches the
|
|
57
|
-
|
|
56
|
+
- Caches the catalog in pi's model store, so startup does not wait on the
|
|
57
|
+
network and a briefly absent relay is covered. The cached models are
|
|
58
|
+
re-pointed at the relay base configured now, so moving the relay does not
|
|
59
|
+
leave requests aimed at the old address.
|
|
58
60
|
- Reuses pi's built-in stream functions for both wires — no custom transport.
|
|
59
|
-
- Registers no commands: the catalog
|
|
61
|
+
- Registers no commands: pi refreshes the catalog on every startup.
|
|
60
62
|
|
|
61
63
|
## Configuration
|
|
62
64
|
|
|
65
|
+
Everything lives in `~/.pi/agent/auth.json`: the relay's API key and the relay
|
|
66
|
+
base it applies to.
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"pengepul": {
|
|
71
|
+
"type": "api_key",
|
|
72
|
+
"key": "sk-local-...",
|
|
73
|
+
"baseUrl": "http://127.0.0.1:8317"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`/login pengepul` writes both fields for you — it asks for the key, then for the
|
|
79
|
+
relay URL, and defaults the URL to `http://127.0.0.1:8317`. Editing the file by
|
|
80
|
+
hand works the same way; `baseUrl` may end in `/v1` or not.
|
|
81
|
+
|
|
82
|
+
To reach a relay on another machine, put that machine's address in `baseUrl`
|
|
83
|
+
(and make the relay listen beyond loopback there: `host: 0.0.0.0` in
|
|
84
|
+
`~/.pengepul/config.yaml` on the relay, or forward the port over SSH). The key
|
|
85
|
+
is the relay's own key — `pengepul config api-key` prints it.
|
|
86
|
+
|
|
87
|
+
Optional overrides, for CI or a one-off shell. Each is a fallback: the
|
|
88
|
+
credential wins when it carries a value.
|
|
89
|
+
|
|
63
90
|
| Setting | Env var | Default |
|
|
64
91
|
|---|---|---|
|
|
65
|
-
| Relay base URL | `PENGEPUL_BASE_URL` | `http://127.0.0.1:8317` |
|
|
66
|
-
| API key | `PENGEPUL_API_KEY` |
|
|
92
|
+
| Relay base URL | `PENGEPUL_BASE_URL` | `http://127.0.0.1:8317`, or the relay's own `config.yaml` |
|
|
93
|
+
| API key | `PENGEPUL_API_KEY` | `api-keys[0]` in `~/.pengepul/config.yaml` |
|
|
67
94
|
| Config path | `PENGEPUL_CONFIG` | `~/.pengepul/config.yaml` |
|
|
68
|
-
|
|
|
95
|
+
| Legacy cache path | `PENGEPUL_MODELS_CACHE` | `<agent-dir>/pengepul-models.json` |
|
|
69
96
|
| Discovery timeout | `PENGEPUL_MODELS_TIMEOUT_MS` | `10000` |
|
|
70
97
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
98
|
+
`PENGEPUL_MODELS_CACHE` names the pre-0.3 cache file. It is read once, when pi's
|
|
99
|
+
model store has no pengepul catalog yet, and never written again.
|
|
100
|
+
|
|
101
|
+
Do not set `providers.pengepul.baseUrl` in `models.json`. pi applies that value
|
|
102
|
+
to every model of the provider, which collapses the two wires onto one URL —
|
|
103
|
+
Anthropic Messages traffic would be sent to `/v1` and Chat Completions traffic
|
|
104
|
+
to `/`.
|
|
74
105
|
|
|
75
106
|
## Notes
|
|
76
107
|
|
|
@@ -81,16 +112,40 @@ is set.
|
|
|
81
112
|
pengepul bills against — displayed costs are upstream list prices.
|
|
82
113
|
- The relay must be running and reachable for discovery to succeed. Without a
|
|
83
114
|
cached catalog on a first start, pengepul models stay unavailable until a
|
|
84
|
-
start with the relay up.
|
|
115
|
+
start with the relay up. A relay that answers with 401 leaves the last known
|
|
116
|
+
catalog registered and logs a warning; discovery does not take pi down with
|
|
117
|
+
it.
|
|
85
118
|
|
|
86
119
|
## Development
|
|
87
120
|
|
|
88
121
|
```sh
|
|
122
|
+
npm install
|
|
89
123
|
bun test
|
|
90
124
|
npx tsc --noEmit
|
|
91
125
|
bun scripts/e2e-live.ts # live e2e against a running relay; sends one tiny completion
|
|
92
126
|
```
|
|
93
127
|
|
|
128
|
+
The `@earendil-works/*` copies `npm install` writes into `node_modules` are for
|
|
129
|
+
`tsc` and the tests only. At runtime pi serves the extension those modules from
|
|
130
|
+
its own bundle (`loader.js` maps `@earendil-works/pi-ai/providers/all` to a
|
|
131
|
+
virtual module), so the local copies exist to be *type-checked against*, not to
|
|
132
|
+
be the ones in play.
|
|
133
|
+
|
|
134
|
+
Nothing here can enforce that the two match: the peer ranges are `*` and the
|
|
135
|
+
host's version is not knowable at install time. The lockfile records whichever
|
|
136
|
+
version was current when it was last refreshed, so the comparison is a manual
|
|
137
|
+
one, worth making whenever a measurement has to be trusted:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
pi --version # host pi release
|
|
141
|
+
cat node_modules/@earendil-works/pi-ai/package.json # local copy
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
When they drift, anything measured from this directory describes a different
|
|
145
|
+
model catalog than the running extension sees — the 0.84.4 copy carried 40
|
|
146
|
+
`:batch` entries where 0.85.1 carries 68 — and local verification quietly
|
|
147
|
+
disagrees with production.
|
|
148
|
+
|
|
94
149
|
## Releasing
|
|
95
150
|
|
|
96
151
|
Releases publish from CI on a version tag. Bump, tag, push:
|
package/extensions/api-key.ts
CHANGED
|
@@ -1,53 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Read pengepul's own API key.
|
|
3
3
|
*
|
|
4
4
|
* pengepul authenticates every request with a static key from its config:
|
|
5
5
|
* `~/.pengepul/config.yaml`, under `api-keys:` (the first is generated on first
|
|
6
6
|
* run and is `sk-local-...`). Clients send it as `Authorization: Bearer <key>`
|
|
7
7
|
* or `x-api-key: <key>`.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
|
-
const DEFAULT_CONFIG_PATH = "~/.pengepul/config.yaml"
|
|
14
|
-
|
|
15
|
-
export type ApiKeySource = "env" | "config" | "none"
|
|
16
|
-
|
|
17
|
-
export interface ApiKeyResolution {
|
|
18
|
-
/** The resolved key, or undefined when none could be found. */
|
|
19
|
-
key?: string
|
|
20
|
-
source: ApiKeySource
|
|
21
|
-
}
|
|
15
|
+
export const DEFAULT_CONFIG_PATH = "~/.pengepul/config.yaml"
|
|
22
16
|
|
|
23
17
|
export const API_KEY_ENV = "PENGEPUL_API_KEY"
|
|
24
18
|
export const CONFIG_PATH_ENV = "PENGEPUL_CONFIG"
|
|
25
19
|
|
|
26
|
-
/**
|
|
27
|
-
* Resolve the key from an env map and a config-file reader.
|
|
28
|
-
*
|
|
29
|
-
* @param env the environment (or a test substitution for it).
|
|
30
|
-
* @param readConfig reads a config file's text by path, or undefined when the
|
|
31
|
-
* path is unwritable/absent. Injected to keep this pure.
|
|
32
|
-
*/
|
|
33
|
-
export function resolveApiKey(
|
|
34
|
-
env: Record<string, string | undefined>,
|
|
35
|
-
readConfig: (path: string) => string | undefined,
|
|
36
|
-
): ApiKeyResolution {
|
|
37
|
-
const envKey = env[API_KEY_ENV]
|
|
38
|
-
if (envKey && envKey.trim() !== "") return { key: envKey, source: "env" }
|
|
39
|
-
|
|
40
|
-
const configPath = env[CONFIG_PATH_ENV] ?? DEFAULT_CONFIG_PATH
|
|
41
|
-
const configText = readConfig(configPath)
|
|
42
|
-
if (configText === undefined) return { source: "none" }
|
|
43
|
-
|
|
44
|
-
const keys = extractApiKeys(configText)
|
|
45
|
-
const first = keys[0]
|
|
46
|
-
if (first) return { key: first, source: "config" }
|
|
47
|
-
|
|
48
|
-
return { source: "none" }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
20
|
/**
|
|
52
21
|
* Extract `api-keys:` entries from pengepul's YAML config, without a YAML
|
|
53
22
|
* dependency. Handles both the inline-flow form and the block-sequence form:
|
package/extensions/config.ts
CHANGED
|
@@ -1,40 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Where the pengepul provider's fallback sources live.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* `auth.json` is the configuration surface. These are the escape hatches kept
|
|
5
|
+
* for tests, CI, and a machine that happens to run the relay itself: env vars
|
|
6
|
+
* for the relay base and key, and pengepul's own config on disk.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { join } from "node:path"
|
|
9
10
|
|
|
10
|
-
import {
|
|
11
|
+
import { CONFIG_PATH_ENV, DEFAULT_CONFIG_PATH } from "./api-key.ts"
|
|
12
|
+
|
|
13
|
+
export { CONFIG_PATH_ENV, DEFAULT_CONFIG_PATH }
|
|
11
14
|
|
|
12
15
|
export const RELAY_BASE_ENV = "PENGEPUL_BASE_URL"
|
|
13
16
|
export const MODELS_CACHE_ENV = "PENGEPUL_MODELS_CACHE"
|
|
14
17
|
export const MODELS_TIMEOUT_MS_ENV = "PENGEPUL_MODELS_TIMEOUT_MS"
|
|
15
18
|
|
|
16
19
|
export interface PengepulSettings {
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
/** Where the
|
|
20
|
-
|
|
21
|
-
/** Discovery timeout in milliseconds. */
|
|
22
|
-
modelsTimeoutMs: number
|
|
20
|
+
/** Where pengepul's own config lives, when this machine runs the relay. */
|
|
21
|
+
configPath: string
|
|
22
|
+
/** Where the pre-0.3 catalog cache lives; read once to seed pi's store. */
|
|
23
|
+
legacyCachePath: string
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export function resolveSettings(
|
|
26
27
|
env: Record<string, string | undefined>,
|
|
27
28
|
agentDir: string,
|
|
28
29
|
): PengepulSettings {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
env[MODELS_CACHE_ENV] ?? join(agentDir, "pengepul-models.json")
|
|
32
|
-
|
|
33
|
-
const parsedTimeout = rawTimeout ? Number(rawTimeout) : NaN
|
|
34
|
-
const modelsTimeoutMs =
|
|
35
|
-
Number.isFinite(parsedTimeout) && parsedTimeout > 0
|
|
36
|
-
? parsedTimeout
|
|
37
|
-
: 10_000
|
|
38
|
-
|
|
39
|
-
return { relayBase, modelsCachePath, modelsTimeoutMs }
|
|
30
|
+
return {
|
|
31
|
+
configPath: env[CONFIG_PATH_ENV] ?? DEFAULT_CONFIG_PATH,
|
|
32
|
+
legacyCachePath: env[MODELS_CACHE_ENV] ?? join(agentDir, "pengepul-models.json"),
|
|
33
|
+
}
|
|
40
34
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pengepul credential and relay-base resolution.
|
|
3
|
+
*
|
|
4
|
+
* The credential is the pengepul entry in pi's `auth.json`: an API key for the
|
|
5
|
+
* relay plus the relay base it applies to. pi core resolves the key itself but
|
|
6
|
+
* knows nothing about `baseUrl`, so the extension reads it here. Everything in
|
|
7
|
+
* this module is pure: an env map and config text in, a value out.
|
|
8
|
+
*
|
|
9
|
+
* Precedence, highest first: credential, then environment, then the relay's own
|
|
10
|
+
* config file (which only exists on the machine running the relay), then the
|
|
11
|
+
* loopback default pengepul binds to.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { normalizeRootBaseUrl } from "./dialect.ts"
|
|
15
|
+
import { DEFAULT_RELAY_BASE } from "./models.ts"
|
|
16
|
+
|
|
17
|
+
/** The fields this provider reads off a stored credential. */
|
|
18
|
+
export interface PengepulCredential {
|
|
19
|
+
/** Always `"api_key"` in pi's store; read as unknown so a hand-edited file cannot lie its way past a cast. */
|
|
20
|
+
type?: unknown
|
|
21
|
+
key?: unknown
|
|
22
|
+
baseUrl?: unknown
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
26
|
+
if (typeof value !== "string") return undefined
|
|
27
|
+
const trimmed = value.trim()
|
|
28
|
+
return trimmed === "" ? undefined : trimmed
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The relay base stored on the credential, when the user set one. */
|
|
32
|
+
export function credentialRelayBase(credential: PengepulCredential | undefined): string | undefined {
|
|
33
|
+
return nonEmptyString(credential?.baseUrl)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The API key stored on the credential, when the user set one. */
|
|
37
|
+
export function credentialApiKey(credential: PengepulCredential | undefined): string | undefined {
|
|
38
|
+
return nonEmptyString(credential?.key)
|
|
39
|
+
}
|
|
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
|
+
export interface RelayBaseSources {
|
|
81
|
+
/** The relay base stored on the pengepul credential. */
|
|
82
|
+
credential?: string | undefined
|
|
83
|
+
/** The `PENGEPUL_BASE_URL` environment value. */
|
|
84
|
+
environment?: string | undefined
|
|
85
|
+
/** The relay base derived from pengepul's own config file. */
|
|
86
|
+
config?: string | undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The relay root every wire appends to: highest-precedence non-blank source, or
|
|
91
|
+
* loopback. A trailing `/v1` is accepted and stripped, so a base copied from a
|
|
92
|
+
* client config cannot produce `/v1/v1` downstream.
|
|
93
|
+
*/
|
|
94
|
+
export function resolveRelayBase(sources: RelayBaseSources): string {
|
|
95
|
+
const chosen =
|
|
96
|
+
nonEmptyString(sources.credential) ??
|
|
97
|
+
nonEmptyString(sources.environment) ??
|
|
98
|
+
nonEmptyString(sources.config) ??
|
|
99
|
+
DEFAULT_RELAY_BASE
|
|
100
|
+
return normalizeRootBaseUrl(chosen)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ApiKeySources {
|
|
104
|
+
/** The key stored on the pengepul credential. */
|
|
105
|
+
credential?: string | undefined
|
|
106
|
+
/** The key resolved from `PENGEPUL_API_KEY` or pengepul's own config file. */
|
|
107
|
+
ambient?: string | undefined
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The key requests and catalog fetches use, or undefined when none is configured. */
|
|
111
|
+
export function resolveApiKey(sources: ApiKeySources): string | undefined {
|
|
112
|
+
return nonEmptyString(sources.credential) ?? nonEmptyString(sources.ambient)
|
|
113
|
+
}
|
package/extensions/index.ts
CHANGED
|
@@ -1,30 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @pwguler/pi-pengepul-provider entry point - the real edge adapter.
|
|
3
3
|
*
|
|
4
|
-
* Registers pengepul as a pi
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Registers pengepul as a pi provider. pengepul is a relay that pools your
|
|
5
|
+
* Claude/Codex subscriptions and speaks both native wires. The provider itself
|
|
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. The
|
|
8
|
+
* pure core is `./credential.ts`, `./dialect.ts`, and `./models.ts`; this file
|
|
9
|
+
* adapts them to the pi ExtensionAPI seam and reads the one file pi cannot.
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
import {
|
|
11
13
|
getAgentDir,
|
|
12
14
|
type ExtensionAPI,
|
|
13
|
-
type ProviderConfig,
|
|
14
15
|
} from "@earendil-works/pi-coding-agent"
|
|
15
16
|
import { getBuiltinModel, getBuiltinModels, getBuiltinProviders } from "@earendil-works/pi-ai/providers/all"
|
|
16
17
|
import { readFileSync } from "node:fs"
|
|
17
18
|
|
|
18
|
-
import { resolveApiKey } from "./api-key.ts"
|
|
19
19
|
import { resolveSettings } from "./config.ts"
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
loadCachedPengepulModels,
|
|
23
|
-
loadPengepulModels,
|
|
24
|
-
toProviderModelConfigs,
|
|
25
|
-
type PengepulModel,
|
|
26
|
-
} from "./models.ts"
|
|
27
|
-
import { createPengepulRuntime } from "./runtime.ts"
|
|
20
|
+
import { catalogIdForms, type PengepulModel } from "./models.ts"
|
|
21
|
+
import { createPengepulProvider } from "./provider.ts"
|
|
28
22
|
|
|
29
23
|
function expandHome(path: string): string {
|
|
30
24
|
if (path === "~") return process.env.HOME ?? path
|
|
@@ -53,11 +47,10 @@ function metaFromModel(model: NonNullable<ReturnType<typeof getBuiltinModel>>) {
|
|
|
53
47
|
}
|
|
54
48
|
|
|
55
49
|
/**
|
|
56
|
-
* Multi-catalog lookup over pi's builtin models. A
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* the first hit; reasoning metadata and the thinkingLevelMap flow from it.
|
|
50
|
+
* Multi-catalog lookup over pi's builtin models. A relay id can match several
|
|
51
|
+
* catalogs, so each of the id's shapes is tried in turn - verbatim, bare under
|
|
52
|
+
* a vendor catalog, last segment, and with the routing namespace removed - and
|
|
53
|
+
* the first hit wins. Reasoning metadata and the thinkingLevelMap flow from it.
|
|
61
54
|
*/
|
|
62
55
|
function createBuiltinLookup(): (id: string, dialect: string) => ReturnType<typeof metaFromModel> | undefined {
|
|
63
56
|
type Entry = { provider: string; id: string };
|
|
@@ -77,70 +70,40 @@ function createBuiltinLookup(): (id: string, dialect: string) => ReturnType<type
|
|
|
77
70
|
}
|
|
78
71
|
|
|
79
72
|
return (id, dialect) => {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
73
|
+
for (const form of catalogIdForms(id)) {
|
|
74
|
+
const candidates: Array<Entry | undefined> = [
|
|
75
|
+
exact.get(form),
|
|
76
|
+
segments.get(form),
|
|
77
|
+
lower.get(form.toLowerCase()),
|
|
78
|
+
]
|
|
79
|
+
for (const candidate of candidates) {
|
|
80
|
+
if (candidate === undefined) continue
|
|
81
|
+
const model = getBuiltinModel(candidate.provider as never, candidate.id as never)
|
|
82
|
+
if (model) return metaFromModel(model)
|
|
83
|
+
}
|
|
91
84
|
}
|
|
92
85
|
return undefined
|
|
93
86
|
}
|
|
94
87
|
}
|
|
95
88
|
|
|
96
|
-
function bareOf(id: string): string {
|
|
97
|
-
const slash = id.lastIndexOf("/")
|
|
98
|
-
return slash === -1 ? id : id.slice(slash + 1)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function createProviderConfigFactory(relayBase: string, apiKey: string | undefined) {
|
|
102
|
-
return (models: readonly PengepulModel[]): ProviderConfig => ({
|
|
103
|
-
name: "Pengepul",
|
|
104
|
-
baseUrl: relayBase,
|
|
105
|
-
apiKey: apiKey ?? "$PENGEPUL_API_KEY",
|
|
106
|
-
api: "anthropic-messages",
|
|
107
|
-
models: toProviderModelConfigs(models, relayBase),
|
|
108
|
-
})
|
|
109
|
-
}
|
|
110
|
-
|
|
111
89
|
/**
|
|
112
|
-
* Model discovery
|
|
113
|
-
*
|
|
114
|
-
*
|
|
90
|
+
* Model discovery belongs to pi: it calls `refreshModels` with the resolved
|
|
91
|
+
* credential, first against pi's cached catalog and then, when the network is
|
|
92
|
+
* allowed, against the relay. Registration is synchronous; nothing here waits
|
|
93
|
+
* on the relay, and the catalog survives a restart through pi's model store.
|
|
115
94
|
*/
|
|
116
|
-
export default
|
|
95
|
+
export default function (pi: ExtensionAPI) {
|
|
117
96
|
const settings = resolveSettings(process.env, getAgentDir())
|
|
118
|
-
const apiKey = resolveApiKey(process.env, readConfigText).key
|
|
119
|
-
|
|
120
|
-
// The relay advertises only ids; context/pricing/modality numbers come from
|
|
121
|
-
// pi's builtin catalogs, searched across providers until one knows the id
|
|
122
|
-
// (aggregator, vendor, and last-segment shapes). The lookup is injected so
|
|
123
|
-
// the catalog logic stays free of pi-ai imports.
|
|
124
|
-
const lookupBuiltin = createBuiltinLookup()
|
|
125
97
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
cachePath: settings.modelsCachePath,
|
|
132
|
-
relayBase: settings.relayBase,
|
|
133
|
-
timeoutMs: settings.modelsTimeoutMs,
|
|
134
|
-
lookupBuiltin,
|
|
135
|
-
signal,
|
|
136
|
-
}),
|
|
137
|
-
loadCachedModels: () => loadCachedPengepulModels(settings.modelsCachePath),
|
|
138
|
-
createProviderConfig: createProviderConfigFactory(settings.relayBase, apiKey),
|
|
98
|
+
const provider = createPengepulProvider({
|
|
99
|
+
env: process.env,
|
|
100
|
+
configText: readConfigText(settings.configPath),
|
|
101
|
+
legacyCachePath: settings.legacyCachePath,
|
|
102
|
+
lookupBuiltin: createBuiltinLookup(),
|
|
139
103
|
})
|
|
140
104
|
|
|
141
|
-
pi.
|
|
142
|
-
runtime.dispose()
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
await runtime.initialize()
|
|
105
|
+
pi.registerProvider(provider)
|
|
146
106
|
}
|
|
107
|
+
|
|
108
|
+
/** The pengepul catalog shape, re-exported for callers that build on the core. */
|
|
109
|
+
export type { PengepulModel }
|