@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
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pengepul provider, as pi-ai sees it.
|
|
3
|
+
*
|
|
4
|
+
* Registered as a native provider, so pi resolves auth through this module and
|
|
5
|
+
* hands the credential back to `refreshModels`. That closes two gaps the
|
|
6
|
+
* config-value form had: model discovery now uses the same resolved key pi uses
|
|
7
|
+
* for requests (it used to read only `PENGEPUL_API_KEY` and pengepul's own
|
|
8
|
+
* config, so a key stored in `auth.json` produced a 401), and the relay base
|
|
9
|
+
* comes from the credential instead of the environment.
|
|
10
|
+
*
|
|
11
|
+
* The relay base lives on the credential as `baseUrl`, a field pi core does not
|
|
12
|
+
* read. Per-model base URLs are derived from it, because pi applies a base URL
|
|
13
|
+
* returned from `resolve()` to every model at once and pengepul's two wires need
|
|
14
|
+
* different paths (`/` for Messages, `/v1` for Chat Completions).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
Api,
|
|
19
|
+
ApiKeyCredential,
|
|
20
|
+
AuthCheck,
|
|
21
|
+
AuthResult,
|
|
22
|
+
Model,
|
|
23
|
+
Provider,
|
|
24
|
+
ProviderAuthInteraction,
|
|
25
|
+
ProviderStreams,
|
|
26
|
+
RefreshModelsContext,
|
|
27
|
+
} from "@earendil-works/pi-ai"
|
|
28
|
+
import { anthropicMessagesApi, lazyStream, openAICompletionsApi } from "@earendil-works/pi-ai/compat"
|
|
29
|
+
|
|
30
|
+
import { API_KEY_ENV, extractApiKeys } from "./api-key.ts"
|
|
31
|
+
import { RELAY_BASE_ENV } from "./config.ts"
|
|
32
|
+
import {
|
|
33
|
+
credentialApiKey,
|
|
34
|
+
credentialRelayBase,
|
|
35
|
+
relayBaseFromConfigText,
|
|
36
|
+
resolveApiKey,
|
|
37
|
+
resolveRelayBase,
|
|
38
|
+
type PengepulCredential,
|
|
39
|
+
} from "./credential.ts"
|
|
40
|
+
import { modelsUrl, type PengepulDialect } from "./dialect.ts"
|
|
41
|
+
import {
|
|
42
|
+
DEFAULT_RELAY_BASE,
|
|
43
|
+
fetchPengepulModels,
|
|
44
|
+
getModelsTimeoutMs,
|
|
45
|
+
isPengepulModelEntry,
|
|
46
|
+
loadCachedPengepulModels,
|
|
47
|
+
PENGEPUL_PROVIDER_ID,
|
|
48
|
+
restampRelayBase,
|
|
49
|
+
toPengepulModels,
|
|
50
|
+
type BuiltinModelLookup,
|
|
51
|
+
type PengepulModelEntry,
|
|
52
|
+
} from "./models.ts"
|
|
53
|
+
|
|
54
|
+
/** The credential this provider writes: the relay key plus the base it applies to. */
|
|
55
|
+
export interface PengepulApiKeyCredential extends ApiKeyCredential {
|
|
56
|
+
type: "api_key"
|
|
57
|
+
key?: string
|
|
58
|
+
baseUrl?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Where the config-file fallback key comes from, for the status label. */
|
|
62
|
+
const CONFIG_FILE_LABEL = "~/.pengepul/config.yaml"
|
|
63
|
+
|
|
64
|
+
export interface PengepulProviderOptions {
|
|
65
|
+
/** Environment map holding the optional `PENGEPUL_*` overrides. */
|
|
66
|
+
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
|
+
/** Catalog fetch transport, for tests. */
|
|
72
|
+
fetchImpl?: typeof fetch
|
|
73
|
+
/** Builtin metadata lookup, injected so the catalog logic stays free of pi imports. */
|
|
74
|
+
lookupBuiltin?: BuiltinModelLookup
|
|
75
|
+
/** Warning sink; defaults to `console.warn`. */
|
|
76
|
+
logWarning?: (message: string) => void
|
|
77
|
+
/** Clock, for tests. */
|
|
78
|
+
now?: () => number
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function errorMessage(error: unknown): string {
|
|
82
|
+
return error instanceof Error ? error.message : String(error)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The `Api` union is open-ended, so narrow it before indexing by wire. */
|
|
86
|
+
function isPengepulDialect(api: Api): api is PengepulDialect {
|
|
87
|
+
return api === "anthropic-messages" || api === "openai-completions"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A provider whose catalog comes from pengepul and whose auth is the relay key.
|
|
92
|
+
*
|
|
93
|
+
* Ambient sources are read from the captured environment and config text, not
|
|
94
|
+
* from `AuthContext.env`, because `refreshModels` receives no auth context and
|
|
95
|
+
* both paths must agree on what the key and base are.
|
|
96
|
+
*/
|
|
97
|
+
export function createPengepulProvider(options: PengepulProviderOptions): Provider {
|
|
98
|
+
const env = options.env ?? process.env
|
|
99
|
+
const logWarning =
|
|
100
|
+
options.logWarning ?? ((message: string) => console.warn(`[pengepul] ${message}`))
|
|
101
|
+
const now = options.now ?? Date.now
|
|
102
|
+
const fetchImpl = options.fetchImpl ?? fetch
|
|
103
|
+
const timeoutMs = getModelsTimeoutMs(env as NodeJS.ProcessEnv)
|
|
104
|
+
|
|
105
|
+
const environmentRelayBase = env[RELAY_BASE_ENV]
|
|
106
|
+
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
|
+
|
|
111
|
+
let models: PengepulModelEntry[] = []
|
|
112
|
+
|
|
113
|
+
/** The key from outside auth.json, with the label the status UI shows. */
|
|
114
|
+
function ambientKey(): { key?: string; source?: string } {
|
|
115
|
+
if (environmentApiKey?.trim()) return { key: environmentApiKey.trim(), source: API_KEY_ENV }
|
|
116
|
+
if (configApiKey) return { key: configApiKey, source: CONFIG_FILE_LABEL }
|
|
117
|
+
return {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resolveKey(credential: unknown): { key?: string; source?: string } {
|
|
121
|
+
const stored = credentialApiKey(credential as PengepulCredential | undefined)
|
|
122
|
+
if (stored) return { key: stored, source: "stored credential" }
|
|
123
|
+
return ambientKey()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const streams: Record<PengepulDialect, ProviderStreams> = {
|
|
127
|
+
"anthropic-messages": anthropicMessagesApi(),
|
|
128
|
+
"openai-completions": openAICompletionsApi(),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function streamsFor(model: Model<Api>): ProviderStreams | undefined {
|
|
132
|
+
return isPengepulDialect(model.api) ? streams[model.api] : undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function unsupported(model: Model<Api>) {
|
|
136
|
+
return lazyStream(model, async () => {
|
|
137
|
+
throw new Error(`pengepul serves no "${model.api}" wire, so ${model.id} cannot be streamed`)
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function restoreOrSeed(context: RefreshModelsContext): Promise<boolean> {
|
|
142
|
+
const relayBase = resolveRelayBase({
|
|
143
|
+
credential: credentialRelayBase(context.credential as PengepulCredential | undefined),
|
|
144
|
+
environment: environmentRelayBase,
|
|
145
|
+
config: configRelayBase,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const stored = (context.stored?.models ?? []).filter(isPengepulModelEntry)
|
|
149
|
+
if (stored.length > 0) {
|
|
150
|
+
const restored = restampRelayBase(stored, relayBase)
|
|
151
|
+
return context.publish({ update: () => { models = restored } })
|
|
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 } })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function refreshFromRelay(context: RefreshModelsContext): Promise<void> {
|
|
163
|
+
const relayBase = resolveRelayBase({
|
|
164
|
+
credential: credentialRelayBase(context.credential as PengepulCredential | undefined),
|
|
165
|
+
environment: environmentRelayBase,
|
|
166
|
+
config: configRelayBase,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const resolved = resolveKey(context.credential)
|
|
170
|
+
if (!resolved.key) {
|
|
171
|
+
logWarning(
|
|
172
|
+
`No pengepul API key is configured (${API_KEY_ENV}, ${CONFIG_FILE_LABEL}, or auth.json), so the model catalog cannot be refreshed. Run /login pengepul.`,
|
|
173
|
+
)
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const catalog = await fetchPengepulModels({
|
|
179
|
+
url: modelsUrl(relayBase),
|
|
180
|
+
apiKey: resolved.key,
|
|
181
|
+
fetchImpl,
|
|
182
|
+
timeoutMs,
|
|
183
|
+
signal: context.signal,
|
|
184
|
+
...(options.lookupBuiltin ? { lookupBuiltin: options.lookupBuiltin } : {}),
|
|
185
|
+
})
|
|
186
|
+
if (context.signal.aborted) return
|
|
187
|
+
|
|
188
|
+
const entries = toPengepulModels(catalog, relayBase)
|
|
189
|
+
await context.publish({
|
|
190
|
+
persist: { models: entries, checkedAt: now() },
|
|
191
|
+
update: () => { models = entries },
|
|
192
|
+
})
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (context.signal.aborted) return
|
|
195
|
+
logWarning(
|
|
196
|
+
`Could not refresh the pengepul model catalog (${errorMessage(error)}). Keeping the last known catalog.`,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
id: PENGEPUL_PROVIDER_ID,
|
|
203
|
+
name: "Pengepul",
|
|
204
|
+
baseUrl: resolveRelayBase({
|
|
205
|
+
environment: environmentRelayBase,
|
|
206
|
+
config: configRelayBase,
|
|
207
|
+
}),
|
|
208
|
+
auth: {
|
|
209
|
+
apiKey: {
|
|
210
|
+
name: "Pengepul relay",
|
|
211
|
+
login: async (
|
|
212
|
+
interaction: ProviderAuthInteraction,
|
|
213
|
+
): Promise<PengepulApiKeyCredential> => {
|
|
214
|
+
const key = (await interaction.prompt({
|
|
215
|
+
type: "secret",
|
|
216
|
+
message: "Pengepul API key",
|
|
217
|
+
})).trim()
|
|
218
|
+
const answer = (await interaction.prompt({
|
|
219
|
+
type: "text",
|
|
220
|
+
message: "Pengepul relay URL",
|
|
221
|
+
placeholder: DEFAULT_RELAY_BASE,
|
|
222
|
+
})).trim()
|
|
223
|
+
// Written, not defaulted at read time: pi replaces the whole
|
|
224
|
+
// credential on login, so the URL has to be in the stored entry or a
|
|
225
|
+
// key rotation silently points the relay back at loopback.
|
|
226
|
+
return {
|
|
227
|
+
type: "api_key",
|
|
228
|
+
...(key === "" ? {} : { key }),
|
|
229
|
+
baseUrl: resolveRelayBase({ credential: answer }),
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
check: async (input): Promise<AuthCheck | undefined> => {
|
|
233
|
+
const resolved = resolveKey(input.credential)
|
|
234
|
+
return resolved.key
|
|
235
|
+
? { type: "api_key", ...(resolved.source ? { source: resolved.source } : {}) }
|
|
236
|
+
: undefined
|
|
237
|
+
},
|
|
238
|
+
resolve: async (input): Promise<AuthResult | undefined> => {
|
|
239
|
+
const resolved = resolveKey(input.credential)
|
|
240
|
+
if (!resolved.key) return undefined
|
|
241
|
+
// No `baseUrl` here on purpose: pi would apply it to every model and
|
|
242
|
+
// collapse the two dialect base URLs into one.
|
|
243
|
+
return {
|
|
244
|
+
auth: { apiKey: resolved.key },
|
|
245
|
+
...(resolved.source ? { source: resolved.source } : {}),
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
getModels: () => models,
|
|
251
|
+
refreshModels: async (context) => {
|
|
252
|
+
if (!(await restoreOrSeed(context))) return
|
|
253
|
+
if (!context.allowNetwork || context.signal.aborted) return
|
|
254
|
+
await refreshFromRelay(context)
|
|
255
|
+
},
|
|
256
|
+
stream: (model, context, streamOptions) => {
|
|
257
|
+
const implementation = streamsFor(model)
|
|
258
|
+
return implementation
|
|
259
|
+
? implementation.stream(model, context, streamOptions)
|
|
260
|
+
: unsupported(model)
|
|
261
|
+
},
|
|
262
|
+
streamSimple: (model, context, streamOptions) => {
|
|
263
|
+
const implementation = streamsFor(model)
|
|
264
|
+
return implementation
|
|
265
|
+
? implementation.streamSimple(model, context, streamOptions)
|
|
266
|
+
: unsupported(model)
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pwguler/pi-pengepul-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "pi custom provider for pengepul, a
|
|
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",
|
|
7
7
|
"author": "pwguler",
|
|
8
8
|
"repository": {
|
package/extensions/runtime.ts
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cached-model runtime for the pengepul provider.
|
|
3
|
-
*
|
|
4
|
-
* Registers the provider immediately from the cache (so startup never waits on
|
|
5
|
-
* the network), refreshes it in the background, and disposes cleanly. There
|
|
6
|
-
* are no user-facing commands: the startup refresh is the only refresh.
|
|
7
|
-
*
|
|
8
|
-
* The pi seam is an interface, so the whole thing is testable with a fake host.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { PengepulModel, PengepulModelSource } from "./models.ts"
|
|
12
|
-
|
|
13
|
-
export interface PengepulRuntimeApi {
|
|
14
|
-
registerProvider(name: string, config: unknown): void
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface PengepulRuntimeOptions {
|
|
18
|
-
/** A provider config built from a model list. */
|
|
19
|
-
createProviderConfig: (models: readonly PengepulModel[]) => unknown
|
|
20
|
-
/** Live fetch; resolves to the catalog plus a source marker. */
|
|
21
|
-
loadModels: (signal: AbortSignal) => Promise<PengepulModelSource>
|
|
22
|
-
/** Cached catalog only; empty when no valid cache exists. */
|
|
23
|
-
loadCachedModels: () => Promise<readonly PengepulModel[]>
|
|
24
|
-
now?: () => number
|
|
25
|
-
logWarning?: (message: string) => void
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface PengepulRefreshResult {
|
|
29
|
-
refreshed: boolean
|
|
30
|
-
source: PengepulModelSource["source"]
|
|
31
|
-
modelCount: number
|
|
32
|
-
warning?: string
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface RuntimeStatus {
|
|
36
|
-
source: PengepulModelSource["source"]
|
|
37
|
-
modelCount: number
|
|
38
|
-
providerRegistered: boolean
|
|
39
|
-
lastSuccess?: number
|
|
40
|
-
lastAttempt?: number
|
|
41
|
-
warning?: string
|
|
42
|
-
refreshing: boolean
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const REDACTED = "[redacted]"
|
|
46
|
-
|
|
47
|
-
function errorMessage(error: unknown): string {
|
|
48
|
-
return error instanceof Error ? error.message : String(error)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function redactDiagnosticText(value: string): string {
|
|
52
|
-
return value
|
|
53
|
-
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, `Bearer ${REDACTED}`)
|
|
54
|
-
.replace(/\b(?:sk-local|sk-|api[-_ ]?key|token|secret|password)[_-]?[\w.~+/=-]{8,}\b/gi, REDACTED)
|
|
55
|
-
.replace(/\b(?:api[-_ ]?key|token|secret|password)\s*[=:]\s*[^\s,;)]+/gi, (match) => {
|
|
56
|
-
const separator = match.match(/\s*[=:]\s*/)?.[0] ?? "="
|
|
57
|
-
return `${match.slice(0, match.indexOf(separator))}${separator}${REDACTED}`
|
|
58
|
-
})
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export class PengepulRuntime {
|
|
62
|
-
private readonly now: () => number
|
|
63
|
-
private readonly logWarning: (message: string) => void
|
|
64
|
-
private status: RuntimeStatus
|
|
65
|
-
private providerRegistered = false
|
|
66
|
-
private refreshPromise: Promise<PengepulRefreshResult> | undefined
|
|
67
|
-
private readonly shutdown = new AbortController()
|
|
68
|
-
|
|
69
|
-
constructor(
|
|
70
|
-
private readonly pi: PengepulRuntimeApi,
|
|
71
|
-
private readonly options: PengepulRuntimeOptions,
|
|
72
|
-
) {
|
|
73
|
-
this.now = options.now ?? Date.now
|
|
74
|
-
this.logWarning = options.logWarning ?? ((message) => console.warn(`[pengepul] ${message}`))
|
|
75
|
-
this.status = {
|
|
76
|
-
source: "empty",
|
|
77
|
-
modelCount: 0,
|
|
78
|
-
providerRegistered: false,
|
|
79
|
-
refreshing: false,
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Registers the cached catalog immediately (so host startup does not wait on
|
|
85
|
-
* the network) and refreshes it in the background. Without a valid cache the
|
|
86
|
-
* live refresh is awaited so models are available right away.
|
|
87
|
-
*/
|
|
88
|
-
async initialize(): Promise<void> {
|
|
89
|
-
const cached = await this.options.loadCachedModels()
|
|
90
|
-
if (cached.length === 0) {
|
|
91
|
-
await this.refresh()
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
this.pi.registerProvider("pengepul", this.options.createProviderConfig(cached))
|
|
96
|
-
this.providerRegistered = true
|
|
97
|
-
this.status = {
|
|
98
|
-
...this.status,
|
|
99
|
-
source: "cache",
|
|
100
|
-
modelCount: cached.length,
|
|
101
|
-
providerRegistered: true,
|
|
102
|
-
lastSuccess: this.now(),
|
|
103
|
-
}
|
|
104
|
-
void this.refresh()
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Aborts any background refresh so a stopping host does not wait on the network. */
|
|
108
|
-
dispose(): void {
|
|
109
|
-
this.shutdown.abort(new Error("pengepul provider shut down"))
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
refresh(): Promise<PengepulRefreshResult> {
|
|
113
|
-
if (this.refreshPromise) return this.refreshPromise
|
|
114
|
-
|
|
115
|
-
const refreshPromise = this.refreshCatalog().finally(() => {
|
|
116
|
-
if (this.refreshPromise === refreshPromise) this.refreshPromise = undefined
|
|
117
|
-
})
|
|
118
|
-
this.refreshPromise = refreshPromise
|
|
119
|
-
return refreshPromise
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private async refreshCatalog(): Promise<PengepulRefreshResult> {
|
|
123
|
-
this.status = { ...this.status, lastAttempt: this.now(), refreshing: true }
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
const loaded = await this.options.loadModels(this.shutdown.signal)
|
|
127
|
-
const warning = loaded.warning ? redactDiagnosticText(loaded.warning) : undefined
|
|
128
|
-
|
|
129
|
-
const shouldRegister =
|
|
130
|
-
!this.providerRegistered ||
|
|
131
|
-
loaded.source === "live" ||
|
|
132
|
-
(this.status.modelCount === 0 && loaded.models.length > 0)
|
|
133
|
-
|
|
134
|
-
if (shouldRegister) {
|
|
135
|
-
this.pi.registerProvider("pengepul", this.options.createProviderConfig(loaded.models))
|
|
136
|
-
this.providerRegistered = true
|
|
137
|
-
|
|
138
|
-
if (loaded.models.length === 0) {
|
|
139
|
-
const preservedWarning = warning ?? "pengepul model discovery returned no models"
|
|
140
|
-
this.status = {
|
|
141
|
-
...this.status,
|
|
142
|
-
source: loaded.source,
|
|
143
|
-
modelCount: 0,
|
|
144
|
-
providerRegistered: true,
|
|
145
|
-
warning: preservedWarning,
|
|
146
|
-
refreshing: false,
|
|
147
|
-
}
|
|
148
|
-
this.warn(preservedWarning)
|
|
149
|
-
return { refreshed: false, source: loaded.source, modelCount: 0, warning: preservedWarning }
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
this.status = {
|
|
153
|
-
...this.status,
|
|
154
|
-
source: loaded.source,
|
|
155
|
-
modelCount: loaded.models.length,
|
|
156
|
-
providerRegistered: true,
|
|
157
|
-
lastSuccess: this.now(),
|
|
158
|
-
warning,
|
|
159
|
-
refreshing: false,
|
|
160
|
-
}
|
|
161
|
-
if (warning) this.warn(warning)
|
|
162
|
-
return { refreshed: true, source: loaded.source, modelCount: loaded.models.length, warning }
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const preservedWarning = warning ?? "pengepul model discovery returned no models"
|
|
166
|
-
this.status = { ...this.status, warning: preservedWarning, refreshing: false }
|
|
167
|
-
this.warn(preservedWarning)
|
|
168
|
-
return {
|
|
169
|
-
refreshed: false,
|
|
170
|
-
source: this.status.source,
|
|
171
|
-
modelCount: this.status.modelCount,
|
|
172
|
-
warning: preservedWarning,
|
|
173
|
-
}
|
|
174
|
-
} catch (error) {
|
|
175
|
-
if (this.shutdown.signal.aborted) {
|
|
176
|
-
this.status = { ...this.status, refreshing: false }
|
|
177
|
-
return {
|
|
178
|
-
refreshed: false,
|
|
179
|
-
source: this.status.source,
|
|
180
|
-
modelCount: this.status.modelCount,
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
const warning = redactDiagnosticText(
|
|
184
|
-
`Could not refresh the pengepul model catalog: ${errorMessage(error)}`,
|
|
185
|
-
)
|
|
186
|
-
this.status = { ...this.status, warning, refreshing: false }
|
|
187
|
-
this.warn(warning)
|
|
188
|
-
return {
|
|
189
|
-
refreshed: false,
|
|
190
|
-
source: this.status.source,
|
|
191
|
-
modelCount: this.status.modelCount,
|
|
192
|
-
warning,
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
private warn(message: string): void {
|
|
198
|
-
try {
|
|
199
|
-
this.logWarning(redactDiagnosticText(message))
|
|
200
|
-
} catch {
|
|
201
|
-
// Diagnostics must never make a catalog refresh fail.
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function createPengepulRuntime(
|
|
207
|
-
pi: PengepulRuntimeApi,
|
|
208
|
-
options: PengepulRuntimeOptions,
|
|
209
|
-
): PengepulRuntime {
|
|
210
|
-
return new PengepulRuntime(pi, options)
|
|
211
|
-
}
|