@evandu/super-pi 0.1.103 → 0.1.105
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/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +6 -6
- package/dist/client/assets/{index-DDkbPYLz.css → index-UiqTcZca.css} +1 -1
- package/dist/client/assets/index-tBfBUY_8.js +74 -0
- package/dist/client/index.html +2 -2
- package/dist/server/application/catalog-service.d.ts +9 -0
- package/dist/server/application/catalog-service.js +11 -0
- package/dist/server/application/conversation-service.d.ts +9 -0
- package/dist/server/application/conversation-service.js +13 -0
- package/dist/server/application/provider-service.d.ts +63 -0
- package/dist/server/application/provider-service.js +136 -0
- package/dist/server/command-handler.d.ts +16 -1
- package/dist/server/command-handler.js +47 -3
- package/dist/server/index.js +13 -1
- package/dist/server/pi-session-events.d.ts +34 -1
- package/dist/server/pi-session-events.js +78 -20
- package/dist/server/pi-session-pool.d.ts +19 -1
- package/dist/server/pi-session-pool.js +25 -3
- package/dist/server/pi-session.d.ts +39 -1
- package/dist/server/pi-session.js +37 -2
- package/dist/server/providers.d.ts +160 -0
- package/dist/server/providers.js +396 -0
- package/dist/server/runtime.d.ts +65 -7
- package/dist/server/runtime.js +76 -13
- package/dist/server/server-context.d.ts +3 -0
- package/dist/server/types.d.ts +72 -0
- package/package.json +6 -1
- package/dist/client/assets/index-mAu39Bra.js +0 -74
|
@@ -78,6 +78,15 @@ export declare class PiSessionPool {
|
|
|
78
78
|
* knows what one looks like.
|
|
79
79
|
*/
|
|
80
80
|
private readonly mintCapability;
|
|
81
|
+
/**
|
|
82
|
+
* How many times the set of models Pi knows about has changed.
|
|
83
|
+
*
|
|
84
|
+
* Injected rather than imported for the same reason the capability minting is: the pool needs to
|
|
85
|
+
* know WHEN a session is stale, and the caller knows what changed. A provider added through the
|
|
86
|
+
* settings window makes every live session unable to reach the new model, and the next turn is
|
|
87
|
+
* where that gets fixed.
|
|
88
|
+
*/
|
|
89
|
+
private readonly modelEpoch;
|
|
81
90
|
private readonly sessions;
|
|
82
91
|
constructor(
|
|
83
92
|
/** Where Pi keeps its session files. */
|
|
@@ -104,7 +113,16 @@ export declare class PiSessionPool {
|
|
|
104
113
|
* does not have to know what a capability is - it needs to know WHEN one is needed, and the caller
|
|
105
114
|
* knows what one looks like.
|
|
106
115
|
*/
|
|
107
|
-
mintCapability?: (conversationId: string) => string
|
|
116
|
+
mintCapability?: (conversationId: string) => string,
|
|
117
|
+
/**
|
|
118
|
+
* How many times the set of models Pi knows about has changed.
|
|
119
|
+
*
|
|
120
|
+
* Injected rather than imported for the same reason the capability minting is: the pool needs to
|
|
121
|
+
* know WHEN a session is stale, and the caller knows what changed. A provider added through the
|
|
122
|
+
* settings window makes every live session unable to reach the new model, and the next turn is
|
|
123
|
+
* where that gets fixed.
|
|
124
|
+
*/
|
|
125
|
+
modelEpoch?: () => number);
|
|
108
126
|
/**
|
|
109
127
|
* The session for a conversation, built if there is none and REPLACED if the live one is stale.
|
|
110
128
|
*
|
|
@@ -48,6 +48,7 @@ export class PiSessionPool {
|
|
|
48
48
|
onSessionDestroyed;
|
|
49
49
|
factory;
|
|
50
50
|
mintCapability;
|
|
51
|
+
modelEpoch;
|
|
51
52
|
sessions = new Map();
|
|
52
53
|
constructor(
|
|
53
54
|
/** Where Pi keeps its session files. */
|
|
@@ -74,13 +75,23 @@ export class PiSessionPool {
|
|
|
74
75
|
* does not have to know what a capability is - it needs to know WHEN one is needed, and the caller
|
|
75
76
|
* knows what one looks like.
|
|
76
77
|
*/
|
|
77
|
-
mintCapability = () => ""
|
|
78
|
+
mintCapability = () => "",
|
|
79
|
+
/**
|
|
80
|
+
* How many times the set of models Pi knows about has changed.
|
|
81
|
+
*
|
|
82
|
+
* Injected rather than imported for the same reason the capability minting is: the pool needs to
|
|
83
|
+
* know WHEN a session is stale, and the caller knows what changed. A provider added through the
|
|
84
|
+
* settings window makes every live session unable to reach the new model, and the next turn is
|
|
85
|
+
* where that gets fixed.
|
|
86
|
+
*/
|
|
87
|
+
modelEpoch = () => 0) {
|
|
78
88
|
this.sessionDir = sessionDir;
|
|
79
89
|
this.repositoryRoot = repositoryRoot;
|
|
80
90
|
this.agentDir = agentDir;
|
|
81
91
|
this.onSessionDestroyed = onSessionDestroyed;
|
|
82
92
|
this.factory = factory;
|
|
83
93
|
this.mintCapability = mintCapability;
|
|
94
|
+
this.modelEpoch = modelEpoch;
|
|
84
95
|
}
|
|
85
96
|
/**
|
|
86
97
|
* The session for a conversation, built if there is none and REPLACED if the live one is stale.
|
|
@@ -188,9 +199,19 @@ export class PiSessionPool {
|
|
|
188
199
|
const same = running?.provider === wanted.provider && (running?.id ?? running?.id) === wanted.modelId;
|
|
189
200
|
if (!same) {
|
|
190
201
|
try {
|
|
191
|
-
|
|
202
|
+
// THE WHOLE MODEL, NOT THE PAIR. Pi's `setModel` puts what it is given straight into the
|
|
203
|
+
// agent's state, so a `{provider, id}` pair would not be a shorthand for a model - it would BE
|
|
204
|
+
// the model the agent runs on, with no `api` and no `contextWindow`. Measured: with the whole
|
|
205
|
+
// model the session reports `api=bedrock-converse-stream`, `contextWindow=1000000` and the
|
|
206
|
+
// thinking levels `[off, minimal, low, medium, high]`; with the pair the api and the context
|
|
207
|
+
// window are null and the levels collapse to `[off]`, which is what the picker shows.
|
|
208
|
+
//
|
|
209
|
+
// The pair is still the fallback, because a session that cannot resolve a model should still
|
|
210
|
+
// be able to run the one it has.
|
|
211
|
+
const whole = session.resolveModel?.(wanted.provider, wanted.modelId);
|
|
212
|
+
await session.setModel(whole ?? { provider: wanted.provider, id: wanted.modelId });
|
|
192
213
|
if (process.env.SUPER_PI_TRACE_LATENCY === "1")
|
|
193
|
-
console.warn(`[super-pi latency] conversation=${request.conversationId} phase=session.model-applied model=${wanted.provider}/${wanted.modelId}`);
|
|
214
|
+
console.warn(`[super-pi latency] conversation=${request.conversationId} phase=session.model-applied model=${wanted.provider}/${wanted.modelId} whole=${whole ? "yes" : "no"}`);
|
|
194
215
|
}
|
|
195
216
|
catch (error) {
|
|
196
217
|
console.warn(`PiSessionPool: conversation ${request.conversationId} asked for ${wanted.provider}/${wanted.modelId} and Pi declined: ${error instanceof Error ? error.message : String(error)}. The turn will run on the model already loaded.`);
|
|
@@ -217,6 +238,7 @@ export class PiSessionPool {
|
|
|
217
238
|
tools: request.tools,
|
|
218
239
|
excludeTools: request.excludeTools,
|
|
219
240
|
extensions: superPiExtensionPaths(this.repositoryRoot),
|
|
241
|
+
modelEpoch: this.modelEpoch(),
|
|
220
242
|
};
|
|
221
243
|
}
|
|
222
244
|
/**
|
|
@@ -68,11 +68,15 @@ export interface SdkCatalog {
|
|
|
68
68
|
*
|
|
69
69
|
* Separate from the models because it is the expensive half: it needs a session, so a caller that
|
|
70
70
|
* only wants the list does not pay for it.
|
|
71
|
+
*
|
|
72
|
+
* UNDEFINED MEANS "THIS RUNTIME DOES NOT KNOW THAT MODEL", and it is not the same answer as "that
|
|
73
|
+
* model has no levels but off". A caller that gets undefined asks whoever does know the model; a
|
|
74
|
+
* caller that got a list would be showing another model's levels.
|
|
71
75
|
*/
|
|
72
76
|
thinkingLevelsFor(model: {
|
|
73
77
|
provider: string;
|
|
74
78
|
modelId: string;
|
|
75
|
-
}): Promise<string[]>;
|
|
79
|
+
}): Promise<string[] | undefined>;
|
|
76
80
|
}
|
|
77
81
|
/**
|
|
78
82
|
* Builds a zero-process catalog reader.
|
|
@@ -125,6 +129,16 @@ export interface SessionFingerprint {
|
|
|
125
129
|
excludeTools: string[] | undefined;
|
|
126
130
|
/** Which of Super Pi's own extensions were mounted. */
|
|
127
131
|
extensions: string[];
|
|
132
|
+
/**
|
|
133
|
+
* How many times the set of models Pi knows about has changed.
|
|
134
|
+
*
|
|
135
|
+
* NOT THE MODEL, BUT THE WORLD THE MODEL LIVES IN. A session's `ModelRuntime` is built with it and
|
|
136
|
+
* does not learn about a provider added afterwards, so a session that keeps running on the old
|
|
137
|
+
* runtime cannot switch to a model that only exists in the new one - and Pi's refusal to switch is
|
|
138
|
+
* swallowed into a warning, which would leave the turn running a model the picker does not name.
|
|
139
|
+
* The counter makes such a session stale, and it is rebuilt at its next turn.
|
|
140
|
+
*/
|
|
141
|
+
modelEpoch: number;
|
|
128
142
|
}
|
|
129
143
|
/**
|
|
130
144
|
* Fingerprints are compared by VALUE, and this is where that comparison is spelled out.
|
|
@@ -235,11 +249,35 @@ export interface PiSessionLike {
|
|
|
235
249
|
* running session, so a model change does not need a new session - and making one would cost a
|
|
236
250
|
* transcript replay, which is the most expensive thing this runtime does. A user who picks a different
|
|
237
251
|
* model in the picker would otherwise pay for the whole conversation's history to be re-read.
|
|
252
|
+
*
|
|
253
|
+
* THE ARGUMENT MUST BE A WHOLE MODEL. Pi's `setModel` puts it straight into the agent's state
|
|
254
|
+
* (`agent-session.ts:2514`), so a `{provider, id}` pair is not a shorthand for a model - it IS the
|
|
255
|
+
* model the agent will use, and it has no `api`, no `contextWindow` and no `maxTokens`. Measured:
|
|
256
|
+
* with a whole model the session reports `api=bedrock-converse-stream`,
|
|
257
|
+
* `contextWindow=1000000` and the thinking levels `[off, minimal, low, medium, high]`; with a pair
|
|
258
|
+
* the api and the context window are null and the levels collapse to `[off]`. `resolveModel` is how
|
|
259
|
+
* the whole model is obtained.
|
|
238
260
|
*/
|
|
239
261
|
setModel?(model: {
|
|
240
262
|
provider: string;
|
|
241
263
|
id: string;
|
|
264
|
+
[key: string]: unknown;
|
|
242
265
|
}): Promise<void>;
|
|
266
|
+
/**
|
|
267
|
+
* The WHOLE model for a provider/id pair, the way Pi's own `set_model` gets it.
|
|
268
|
+
*
|
|
269
|
+
* Pi's RPC mode looks the pair up in the model runtime's snapshot and hands the FOUND MODEL to
|
|
270
|
+
* `session.setModel` (`rpc-mode.ts:470-477`); the pair alone is never what Pi takes. This is the same
|
|
271
|
+
* lookup, exposed so the caller does not have to own a second model runtime.
|
|
272
|
+
*
|
|
273
|
+
* Optional because a test double has no models; its absence means "this session cannot resolve a
|
|
274
|
+
* model", and the caller then falls back to the pair it has.
|
|
275
|
+
*/
|
|
276
|
+
resolveModel?(provider: string, modelId: string): {
|
|
277
|
+
provider: string;
|
|
278
|
+
id: string;
|
|
279
|
+
[key: string]: unknown;
|
|
280
|
+
} | undefined;
|
|
243
281
|
/** The thinking levels the session's current model supports. Pi's own answer, not a derivation. */
|
|
244
282
|
getAvailableThinkingLevels?(): string[];
|
|
245
283
|
setThinkingLevel?(level: string): void;
|
|
@@ -103,6 +103,14 @@ export async function createSdkCatalog(cwd) {
|
|
|
103
103
|
const cached = levelsByModel.get(key);
|
|
104
104
|
if (cached)
|
|
105
105
|
return cached;
|
|
106
|
+
// THE MODEL HAS TO BE FOUND FIRST, and the reason it has to be found is that a session created
|
|
107
|
+
// WITHOUT one runs on Pi's DEFAULT model - and then it answers with the DEFAULT MODEL'S LEVELS.
|
|
108
|
+
// That is a silent wrong answer: the picker would offer the levels of a model the person is not
|
|
109
|
+
// using, and hide the ones they are. A model this runtime does not know is not a model with no
|
|
110
|
+
// levels; it is a model whose levels have to be asked of whoever does know it.
|
|
111
|
+
const whole = modelRuntime.getModel?.(model.provider, model.modelId);
|
|
112
|
+
if (!whole)
|
|
113
|
+
return undefined;
|
|
106
114
|
// A model that is not running has no levels of its own, so a short-lived in-memory session is
|
|
107
115
|
// built to ask Pi. It costs a session - measured at ~30ms warm, and no process at all - and it is
|
|
108
116
|
// paid once per model rather than once per picker open.
|
|
@@ -112,7 +120,7 @@ export async function createSdkCatalog(cwd) {
|
|
|
112
120
|
modelRuntime,
|
|
113
121
|
settingsManager,
|
|
114
122
|
sessionManager: sdk.SessionManager.inMemory(cwd, { id: "catalog-levels" }),
|
|
115
|
-
model:
|
|
123
|
+
model: whole,
|
|
116
124
|
});
|
|
117
125
|
try {
|
|
118
126
|
const levels = sessionLevels(session);
|
|
@@ -177,6 +185,8 @@ export function sameFingerprint(a, b) {
|
|
|
177
185
|
return false;
|
|
178
186
|
if (!sameSet(a.extensions, b.extensions))
|
|
179
187
|
return false;
|
|
188
|
+
if (a.modelEpoch !== b.modelEpoch)
|
|
189
|
+
return false;
|
|
180
190
|
return true;
|
|
181
191
|
}
|
|
182
192
|
function sameSet(a, b) {
|
|
@@ -269,7 +279,23 @@ export async function createPiSession(creation) {
|
|
|
269
279
|
};
|
|
270
280
|
const { session } = await sdk.createAgentSession(options);
|
|
271
281
|
traceSession(creation.sessionId, "sdk.session.created", startedAt);
|
|
272
|
-
|
|
282
|
+
const live = session;
|
|
283
|
+
// THE WHOLE MODEL, LOOKED UP THE WAY PI LOOKS IT UP. Pi's `setModel` puts the model it is given
|
|
284
|
+
// straight into the agent's state, so what it needs is the WHOLE model - the one with an `api`, a
|
|
285
|
+
// `contextWindow` and a `maxTokens` - and not the `{provider, id}` pair the picker holds. The only
|
|
286
|
+
// thing that has a whole model is the model runtime this session was built with, and Pi's own RPC
|
|
287
|
+
// `set_model` does exactly this lookup before it calls `setModel` (`rpc-mode.ts:470-477`).
|
|
288
|
+
//
|
|
289
|
+
// It is attached to the session rather than kept beside it because the session is what outlives a
|
|
290
|
+
// turn: a model change applies to a LIVE session, and the live session is the thing that has to be
|
|
291
|
+
// able to answer "give me this model whole".
|
|
292
|
+
live.resolveModel = (provider, modelId) => {
|
|
293
|
+
const found = modelRuntime.getModel?.(provider, modelId);
|
|
294
|
+
if (!found || typeof found !== "object")
|
|
295
|
+
return undefined;
|
|
296
|
+
return found;
|
|
297
|
+
};
|
|
298
|
+
return live;
|
|
273
299
|
});
|
|
274
300
|
}
|
|
275
301
|
function transcriptBytes(sessionManager) {
|
|
@@ -319,8 +345,17 @@ async function resolveSessionManager(sdk, creation) {
|
|
|
319
345
|
* file goes out of its way not to do for a path it can derive. The derivation is Pi's own default:
|
|
320
346
|
* `~/.pi/agent`, with the home directory taken from the environment rather than from `os.homedir()`,
|
|
321
347
|
* because on Windows the two disagree often enough to matter.
|
|
348
|
+
*
|
|
349
|
+
* `PI_CODING_AGENT_DIR` WINS, exactly as it wins in Pi (`core/config.ts:getAgentDir`). A person who
|
|
350
|
+
* set it expects Pi to read its whole configuration from there, and this function used to disagree
|
|
351
|
+
* with Pi about it: Pi's model runtime read `models.json` from that directory while the session's
|
|
352
|
+
* resource loader read the skills from `~/.pi/agent`. Two halves of one Pi pointed at two
|
|
353
|
+
* directories, and only one of them was the one the person had configured.
|
|
322
354
|
*/
|
|
323
355
|
function defaultAgentDir() {
|
|
356
|
+
const configured = process.env.PI_CODING_AGENT_DIR?.trim();
|
|
357
|
+
if (configured)
|
|
358
|
+
return configured;
|
|
324
359
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
325
360
|
return home ? join(home, ".pi", "agent") : join(".pi", "agent");
|
|
326
361
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { PublicProvider } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* THE PROVIDERS A PERSON ADDED, and the two files that make Pi know about them.
|
|
4
|
+
*
|
|
5
|
+
* WHAT THIS REPLACES. Pi's own answer to "I want to use a model that is not in your list" is a
|
|
6
|
+
* hand-written `~/.pi/agent/models.json` (the provider and model definitions) plus a key in
|
|
7
|
+
* `~/.pi/agent/auth.json`. There is no command for it and no UI for it - the files are the interface.
|
|
8
|
+
* This module is that interface, in a form: the settings window collects the same fields and writes
|
|
9
|
+
* the same files.
|
|
10
|
+
*
|
|
11
|
+
* WHY THE FILES AND NOT A RUNTIME REGISTRATION. Pi's SDK can register a provider in memory
|
|
12
|
+
* (`ModelRuntime.registerProvider`) and hold a key in memory (`setRuntimeApiKey`), and both are
|
|
13
|
+
* tempting - they touch nothing of the user's. They do not work here, for two reasons that are
|
|
14
|
+
* facts about this codebase rather than opinions:
|
|
15
|
+
*
|
|
16
|
+
* 1. `ModelRuntime` is created PER SESSION here (`pi-session.ts:498`), so a provider registered on
|
|
17
|
+
* one instance is invisible to the next conversation.
|
|
18
|
+
* 2. A spawned Pi (`pi --mode rpc`, the one-shot mode) has no provider-registration command at all
|
|
19
|
+
* - its RPC surface is `set_model` and `get_available_models` and nothing that defines a
|
|
20
|
+
* provider. A provider that exists only in this process's memory cannot reach it.
|
|
21
|
+
*
|
|
22
|
+
* The files, by contrast, are read by BOTH paths: the SDK's `ModelRuntime.create()` loads
|
|
23
|
+
* `models.json` and `auth.json` from Pi's agent dir, and a spawned Pi does the same. So writing them
|
|
24
|
+
* is the only way one setting works everywhere - including in the terminal, where `pi` sees the same
|
|
25
|
+
* provider the person just added here.
|
|
26
|
+
*
|
|
27
|
+
* THE TRUTH SOURCE IS OURS, THE FILES ARE DERIVED. `<dataDir>/providers.json` (0600, holds the key)
|
|
28
|
+
* says which providers Super Pi manages. Pi's two files are rewritten from it after every change, by
|
|
29
|
+
* MERGING: only the provider ids we manage are touched, everything else in those files is left alone.
|
|
30
|
+
* That is what makes "delete the provider I added" safe, and it is why a person who hand-wrote their
|
|
31
|
+
* own provider keeps it.
|
|
32
|
+
*
|
|
33
|
+
* THE KEY NEVER REACHES THE BROWSER. It lives in two files with restrictive permissions - ours and
|
|
34
|
+
* Pi's `auth.json` - and in neither the state snapshot nor any event. What the browser gets is
|
|
35
|
+
* `hasKey: true`.
|
|
36
|
+
*/
|
|
37
|
+
/** One model of a provider, as the settings window collects it. */
|
|
38
|
+
export type ProviderModelDefinition = {
|
|
39
|
+
id: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
reasoning?: boolean;
|
|
42
|
+
imageInput?: boolean;
|
|
43
|
+
contextWindow?: number;
|
|
44
|
+
maxTokens?: number;
|
|
45
|
+
/**
|
|
46
|
+
* The two compatibility switches an OpenAI-compatible server sometimes needs.
|
|
47
|
+
*
|
|
48
|
+
* `supportsDeveloperRole: false` makes Pi send the system prompt as a `system` message instead of
|
|
49
|
+
* a `developer` one, which is what relays and local servers that predate the `developer` role need.
|
|
50
|
+
* `supportsReasoningEffort: false` stops Pi from sending `reasoning_effort` to a server that does
|
|
51
|
+
* not know the field. Both are per-provider in Pi's schema and per-model here, because a server can
|
|
52
|
+
* be one and not the other.
|
|
53
|
+
*/
|
|
54
|
+
supportsDeveloperRole?: boolean;
|
|
55
|
+
supportsReasoningEffort?: boolean;
|
|
56
|
+
};
|
|
57
|
+
/** One provider, as the settings window collects it. */
|
|
58
|
+
export type ProviderDefinition = {
|
|
59
|
+
id: string;
|
|
60
|
+
name?: string;
|
|
61
|
+
baseUrl: string;
|
|
62
|
+
api: string;
|
|
63
|
+
/** The API key. Stored, never broadcast. */
|
|
64
|
+
apiKey?: string;
|
|
65
|
+
models: ProviderModelDefinition[];
|
|
66
|
+
};
|
|
67
|
+
/** One model, as the browser sees it. No key, no nothing secret - there is nothing secret here. */
|
|
68
|
+
export type { PublicProvider, PublicProviderModel } from "./types.js";
|
|
69
|
+
/**
|
|
70
|
+
* The API adapters Pi ships, as the ids a provider may name.
|
|
71
|
+
*
|
|
72
|
+
* Taken from Pi's own `KnownApi` union (`pi-ai/dist/types.d.ts`) rather than invented: the id ends
|
|
73
|
+
* up in `models.json` and Pi looks the adapter up by exactly this string, and an unknown one fails
|
|
74
|
+
* at REQUEST time ("No API provider registered for api: ...") rather than at load time. Offering the
|
|
75
|
+
* list in the form is the difference between a person finding the right value and guessing.
|
|
76
|
+
*/
|
|
77
|
+
export declare const KNOWN_APIS: readonly ["openai-completions", "anthropic-messages", "openai-responses", "azure-openai-responses", "openai-codex-responses", "mistral-conversations", "google-generative-ai", "google-vertex", "bedrock-converse-stream", "pi-messages"];
|
|
78
|
+
/**
|
|
79
|
+
* Pi's global configuration directory - where `models.json` and `auth.json` live.
|
|
80
|
+
*
|
|
81
|
+
* `PI_CODING_AGENT_DIR` wins, because that is Pi's own rule (`core/config.js:getAgentDir`) and a
|
|
82
|
+
* person who set it expects Pi to read its config from there. The fallback is Pi's own default,
|
|
83
|
+
* `~/.pi/agent`, with the home directory taken from the environment rather than from `os.homedir()`
|
|
84
|
+
* - on Windows the two disagree often enough to matter.
|
|
85
|
+
*/
|
|
86
|
+
export declare function piAgentDir(): string;
|
|
87
|
+
/** Pi's provider and model definitions. */
|
|
88
|
+
export declare function piModelsPath(agentDir?: string): string;
|
|
89
|
+
/** Pi's credential store. */
|
|
90
|
+
export declare function piAuthPath(agentDir?: string): string;
|
|
91
|
+
/**
|
|
92
|
+
* Strips JSONC comments, the way Pi's own parser does.
|
|
93
|
+
*
|
|
94
|
+
* Pi reads `models.json` with comments allowed, so a person may well have written comments in it -
|
|
95
|
+
* and Super Pi has to be able to READ that file to merge into it. A naive strip would corrupt a
|
|
96
|
+
* string that contains `//` (a base URL with a path, an api key with a slash), so this walks the
|
|
97
|
+
* text and tracks whether it is inside a string.
|
|
98
|
+
*/
|
|
99
|
+
export declare function stripJsonComments(text: string): string;
|
|
100
|
+
/**
|
|
101
|
+
* The providers Super Pi manages, and the writes that make Pi know about them.
|
|
102
|
+
*
|
|
103
|
+
* The class owns three files: its own registry, Pi's `models.json` and Pi's `auth.json`. Only the
|
|
104
|
+
* first is a truth source; the other two are rewritten from it.
|
|
105
|
+
*/
|
|
106
|
+
export declare class ProviderRegistry {
|
|
107
|
+
private readonly dataDir;
|
|
108
|
+
private readonly piDir;
|
|
109
|
+
/**
|
|
110
|
+
* `piDir` is Pi's own configuration directory, and it is a PARAMETER rather than a lookup at
|
|
111
|
+
* write time for one reason: a test that forgets to set `PI_CODING_AGENT_DIR` would then write to
|
|
112
|
+
* the developer's real `~/.pi/agent`. With the directory in hand, a test cannot reach it by
|
|
113
|
+
* accident - it has to be given one.
|
|
114
|
+
*/
|
|
115
|
+
constructor(dataDir: string, piDir?: string);
|
|
116
|
+
/** Super Pi's own registry: the providers it manages, with their keys. */
|
|
117
|
+
private get registryPath();
|
|
118
|
+
/**
|
|
119
|
+
* Reads the registry.
|
|
120
|
+
*
|
|
121
|
+
* A missing file is an empty registry, not an error: nobody has added a provider yet. A file that
|
|
122
|
+
* cannot be parsed IS an error, because reading it as empty would let the next write replace
|
|
123
|
+
* somebody's keys with nothing.
|
|
124
|
+
*/
|
|
125
|
+
load(): Promise<ProviderDefinition[]>;
|
|
126
|
+
/** Writes the registry. The key lives here, so the file is 0600. */
|
|
127
|
+
save(providers: ProviderDefinition[]): Promise<void>;
|
|
128
|
+
/**
|
|
129
|
+
* Makes Pi's files match the registry.
|
|
130
|
+
*
|
|
131
|
+
* `previousIds` are the providers Super Pi managed before this change: they are the only ones
|
|
132
|
+
* whose entries may be REMOVED. An id that is in Pi's `models.json` and not in `previousIds` belongs
|
|
133
|
+
* to the person who wrote that file, and touching it would be Super Pi editing a provider it does
|
|
134
|
+
* not own - so the write is refused with a message naming the id.
|
|
135
|
+
*/
|
|
136
|
+
applyToPi(previous: ProviderDefinition[], next: ProviderDefinition[]): Promise<void>;
|
|
137
|
+
/**
|
|
138
|
+
* Writes the keys into Pi's `auth.json`.
|
|
139
|
+
*
|
|
140
|
+
* A credential is REMOVED only when it is the one Super Pi wrote: the same provider id and the
|
|
141
|
+
* same key. A person who replaced our key with their own - or who ran Pi's `/login` and put an
|
|
142
|
+
* OAuth token there - keeps it. Deleting somebody else's credential because they removed our
|
|
143
|
+
* provider would be the worst version of this feature.
|
|
144
|
+
*
|
|
145
|
+
* TWO CASES, and the second is the one that is easy to miss: the provider can be gone ENTIRELY
|
|
146
|
+
* (it was removed), or it can still be there with its key GONE (the person asked for the key to be
|
|
147
|
+
* cleared). Both have to take the credential with them, or "clear the key" would leave a working
|
|
148
|
+
* key sitting in Pi's auth file.
|
|
149
|
+
*/
|
|
150
|
+
private applyKeysToPi;
|
|
151
|
+
}
|
|
152
|
+
/** The provider list as the browser sees it: the key becomes a fact. */
|
|
153
|
+
export declare function publicProviders(providers: ProviderDefinition[]): PublicProvider[];
|
|
154
|
+
/**
|
|
155
|
+
* Checks what the form collected and returns it in the shape the registry stores.
|
|
156
|
+
*
|
|
157
|
+
* EVERY RULE HERE SAYS WHY, because a message a person reads is the only validation they get: the
|
|
158
|
+
* server cannot ask a follow-up question, so a refusal has to name the field and the reason.
|
|
159
|
+
*/
|
|
160
|
+
export declare function validateProvider(input: unknown): ProviderDefinition;
|