@evandu/super-pi 0.1.123 → 0.1.125
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 +7 -7
- package/dist/client/assets/{CodeMirrorEditor-B2AXs2YB.js → CodeMirrorEditor-CK5tZxIo.js} +2 -2
- package/dist/client/assets/{dist-CRpnXCSn.js → dist-5O-klFrP.js} +1 -1
- package/dist/client/assets/{dist-D11ifrNu.js → dist-B8X48E9G.js} +1 -1
- package/dist/client/assets/{dist-Wq0CGa1P.js → dist-BC3_yB3K.js} +1 -1
- package/dist/client/assets/{dist-CrYrGCRr.js → dist-BDAJ6COK.js} +1 -1
- package/dist/client/assets/{dist-DOQGcum7.js → dist-BDULvJIt.js} +1 -1
- package/dist/client/assets/{dist-DOuFuHYr.js → dist-BR5dccfj.js} +1 -1
- package/dist/client/assets/{dist-CvUxw4HU.js → dist-BbK-QOz1.js} +1 -1
- package/dist/client/assets/{dist-CoqrRbfm.js → dist-Ck1wVFJ1.js} +1 -1
- package/dist/client/assets/{dist-B0G3TOfE.js → dist-CrnIVt5K.js} +1 -1
- package/dist/client/assets/{dist-DC4KdKrw.js → dist-D87R6Wol.js} +1 -1
- package/dist/client/assets/{dist-D899ixlt.js → dist-D90mwiSm.js} +1 -1
- package/dist/client/assets/{dist-4hqZVn6B.js → dist-D9IZ5pHN.js} +1 -1
- package/dist/client/assets/{dist-Ip0mRDF1.js → dist-DIa1uLNH.js} +1 -1
- package/dist/client/assets/{dist-CWcEOVNV.js → dist-DR8q7BI-.js} +1 -1
- package/dist/client/assets/{dist-GQ99vtKx.js → dist-Did2mkbm.js} +1 -1
- package/dist/client/assets/{dist-CjyJqaHj.js → dist-DsObObmI.js} +1 -1
- package/dist/client/assets/{dist-BqlvAjyT.js → dist-DsY1TPps.js} +1 -1
- package/dist/client/assets/{dist--hp7cqEH.js → dist-MYmiV3U3.js} +1 -1
- package/dist/client/assets/{dist-Dp9eqvWE.js → dist-Om17wCc8.js} +1 -1
- package/dist/client/assets/{dist-Cxo9Qf8Y.js → dist-fEUHAgwX.js} +1 -1
- package/dist/client/assets/{dist-CcAEnfzB.js → dist-nSf0tcKa.js} +1 -1
- package/dist/client/assets/{dist-CAFUK38E.js → dist-r_bnlg1k.js} +1 -1
- package/dist/client/assets/{index-CCnIvjP6.css → index-CBPWeTvl.css} +1 -1
- package/dist/client/assets/index-qTPZ6eno.js +99 -0
- package/dist/client/index.html +2 -2
- package/dist/server/application/provider-service.d.ts +51 -2
- package/dist/server/application/provider-service.js +166 -11
- package/dist/server/application/run-service.js +14 -7
- package/dist/server/command-handler.js +21 -0
- package/dist/server/http-server.js +8 -0
- package/dist/server/index.js +2 -0
- package/dist/server/pi-session-events.d.ts +6 -1
- package/dist/server/providers.d.ts +106 -0
- package/dist/server/providers.js +247 -35
- package/dist/server/runtime.d.ts +1 -1
- package/dist/server/runtime.js +42 -20
- package/dist/server/store.d.ts +15 -0
- package/dist/server/store.js +34 -0
- package/dist/server/terminal.d.ts +51 -0
- package/dist/server/terminal.js +81 -12
- package/dist/server/types.d.ts +43 -0
- package/package.json +3 -1
- package/dist/client/assets/index-B0TMoUBp.js +0 -99
package/dist/client/index.html
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="alternate icon" href="/favicon.ico" />
|
|
9
9
|
<link rel="apple-touch-icon" href="/favicon.svg" />
|
|
10
10
|
<title>Super Pi</title>
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-qTPZ6eno.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CBPWeTvl.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
@@ -33,8 +33,29 @@ export declare class ProviderService {
|
|
|
33
33
|
private readonly catalogService;
|
|
34
34
|
private readonly store;
|
|
35
35
|
constructor(registry: ProviderRegistry, runtime: AgentRuntime, broadcaster: EventBroadcaster, catalogService: CatalogService, store: Store);
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Every provider Pi knows about, as the browser may see it: no keys, only the fact of one.
|
|
38
|
+
*
|
|
39
|
+
* THREE LISTS, ONE VIEW, because this panel is a view of Pi's model configuration and Pi's
|
|
40
|
+
* configuration is in three places:
|
|
41
|
+
* - `models.json` entries (custom providers, whether this app wrote them or the person did);
|
|
42
|
+
* - credentials in `auth.json` with no `models.json` entry (Pi's BUILT-IN providers);
|
|
43
|
+
* - the ones turned off, which are in neither of Pi's files any more but are kept here.
|
|
44
|
+
* A panel built on the first list alone could never answer "turn off my lapsed coding plan",
|
|
45
|
+
* because that provider is a built-in whose only configuration is a credential.
|
|
46
|
+
*/
|
|
37
47
|
list(): Promise<PublicProvider[]>;
|
|
48
|
+
/**
|
|
49
|
+
* Turns a provider off, then makes every LIVE conversation stop pointing at the models it removed.
|
|
50
|
+
*
|
|
51
|
+
* WHY THE CONVERSATIONS ARE REPAIRED. A conversation's model is copied at birth and never re-read
|
|
52
|
+
* from the workspace - that is the rule. But the model it copied can CEASE TO EXIST, and a
|
|
53
|
+
* conversation holding one does not fail cleanly: Pi runs the turn on its own default, so the
|
|
54
|
+
* picker names one model while another one answers. Measured before this change: removing the
|
|
55
|
+
* model in use left the picker empty (`source=unavailable, models=0`).
|
|
56
|
+
*/
|
|
57
|
+
disable(providerId: string): Promise<PublicProvider[]>;
|
|
58
|
+
enable(providerId: string): Promise<PublicProvider[]>;
|
|
38
59
|
/**
|
|
39
60
|
* Adds a provider, or adds a model to one, or updates one.
|
|
40
61
|
*
|
|
@@ -49,7 +70,17 @@ export declare class ProviderService {
|
|
|
49
70
|
* asks for it to go.
|
|
50
71
|
*/
|
|
51
72
|
set(input: unknown): Promise<PublicProvider[]>;
|
|
52
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Removes a provider for good.
|
|
75
|
+
*
|
|
76
|
+
* THREE KINDS, ONE VERB, because the panel is a view of Pi's model configuration and deleting a
|
|
77
|
+
* provider means the same thing in all three cases:
|
|
78
|
+
* - a provider in `models.json`: its entry and its key go (the original behaviour);
|
|
79
|
+
* - a BUILT-IN with only a credential: its models are Pi's, so the credential is the only thing
|
|
80
|
+
* there is to delete - and deleting it is exactly what makes Pi stop offering it;
|
|
81
|
+
* - one that is already switched off: the remembered copy goes, which is what makes "off"
|
|
82
|
+
* permanent instead of something that would come back months later.
|
|
83
|
+
*/
|
|
53
84
|
remove(providerId: string): Promise<PublicProvider[]>;
|
|
54
85
|
/**
|
|
55
86
|
* Removes ONE model from a provider, leaving the provider and its other models alone.
|
|
@@ -74,6 +105,24 @@ export declare class ProviderService {
|
|
|
74
105
|
* those rules forbid.
|
|
75
106
|
*/
|
|
76
107
|
private afterChange;
|
|
108
|
+
/**
|
|
109
|
+
* Drops a conversation's model when the change removed it.
|
|
110
|
+
*
|
|
111
|
+
* WHY THIS EXISTS AT ALL. A conversation whose model was just deleted is not merely stale - it is
|
|
112
|
+
* WRONG in a way that never surfaces: Pi cannot resolve the model and runs the turn on its own
|
|
113
|
+
* default, so the picker says one thing while another model answers. Clearing the field is the
|
|
114
|
+
* only honest state: the conversation follows Pi's default again, which is what the next turn
|
|
115
|
+
* does anyway.
|
|
116
|
+
*
|
|
117
|
+
* THE LIST IS PASSED IN, NOT DERIVED FROM A CATALOG READ. The caller knows exactly which models it
|
|
118
|
+
* just took away (one model, or a whole provider), so asking Pi what it offers would be a slower
|
|
119
|
+
* and less precise way to learn the same thing - and on a machine where Pi is slow or broken, the
|
|
120
|
+
* broadcast every open tab is waiting for would wait with it.
|
|
121
|
+
*
|
|
122
|
+
* ARCHIVED CONVERSATIONS ARE LEFT ALONE: they are read-only by design, and rewriting a frozen
|
|
123
|
+
* record's stored model would make the archive describe a past that did not happen.
|
|
124
|
+
*/
|
|
125
|
+
private repairConversationModels;
|
|
77
126
|
/**
|
|
78
127
|
* Discovers available models from the remote provider endpoint.
|
|
79
128
|
* If apiKey is omitted but providerId is given, uses the stored key.
|
|
@@ -34,9 +34,106 @@ export class ProviderService {
|
|
|
34
34
|
this.catalogService = catalogService;
|
|
35
35
|
this.store = store;
|
|
36
36
|
}
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Every provider Pi knows about, as the browser may see it: no keys, only the fact of one.
|
|
39
|
+
*
|
|
40
|
+
* THREE LISTS, ONE VIEW, because this panel is a view of Pi's model configuration and Pi's
|
|
41
|
+
* configuration is in three places:
|
|
42
|
+
* - `models.json` entries (custom providers, whether this app wrote them or the person did);
|
|
43
|
+
* - credentials in `auth.json` with no `models.json` entry (Pi's BUILT-IN providers);
|
|
44
|
+
* - the ones turned off, which are in neither of Pi's files any more but are kept here.
|
|
45
|
+
* A panel built on the first list alone could never answer "turn off my lapsed coding plan",
|
|
46
|
+
* because that provider is a built-in whose only configuration is a credential.
|
|
47
|
+
*/
|
|
38
48
|
async list() {
|
|
39
|
-
|
|
49
|
+
const [configured, credentialOnly, disabled] = await Promise.all([
|
|
50
|
+
this.registry.load(),
|
|
51
|
+
this.registry.loadCredentialOnly(),
|
|
52
|
+
this.registry.loadDisabled(),
|
|
53
|
+
]);
|
|
54
|
+
// A built-in's models live in Pi's own catalogue, so the file layer cannot know them. They are
|
|
55
|
+
// filled in from the catalog the panel already receives, which is the only thing that knows what
|
|
56
|
+
// a built-in actually offers - and a provider card with an empty model list would misdescribe a
|
|
57
|
+
// provider that is working perfectly.
|
|
58
|
+
if (credentialOnly.length) {
|
|
59
|
+
try {
|
|
60
|
+
const catalog = await this.runtime.catalog();
|
|
61
|
+
const byProvider = new Map();
|
|
62
|
+
for (const model of catalog.models) {
|
|
63
|
+
const group = byProvider.get(model.provider) ?? [];
|
|
64
|
+
group.push(model);
|
|
65
|
+
byProvider.set(model.provider, group);
|
|
66
|
+
}
|
|
67
|
+
for (const provider of credentialOnly) {
|
|
68
|
+
provider.models = (byProvider.get(provider.id) ?? []).map((model) => ({
|
|
69
|
+
id: model.modelId,
|
|
70
|
+
name: model.label,
|
|
71
|
+
reasoning: model.reasoning,
|
|
72
|
+
imageInput: model.imageInput,
|
|
73
|
+
contextWindow: model.contextWindow,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// A catalog that cannot be read leaves a built-in's models unknown, which is the truth - the
|
|
79
|
+
// provider itself is still listed and can still be switched off.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const offList = disabled.map((entry) => ({
|
|
83
|
+
id: entry.id,
|
|
84
|
+
...(entry.name ? { name: entry.name } : {}),
|
|
85
|
+
baseUrl: typeof entry.modelsEntry?.baseUrl === "string" ? entry.modelsEntry.baseUrl : "",
|
|
86
|
+
api: typeof entry.modelsEntry?.api === "string" ? entry.modelsEntry.api : "",
|
|
87
|
+
hasAuthKey: Boolean(entry.credential),
|
|
88
|
+
...(entry.modelsEntry ? {} : { builtin: true }),
|
|
89
|
+
disabled: true,
|
|
90
|
+
models: entry.models,
|
|
91
|
+
}));
|
|
92
|
+
return publicProviders([...configured, ...credentialOnly, ...offList]);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Turns a provider off, then makes every LIVE conversation stop pointing at the models it removed.
|
|
96
|
+
*
|
|
97
|
+
* WHY THE CONVERSATIONS ARE REPAIRED. A conversation's model is copied at birth and never re-read
|
|
98
|
+
* from the workspace - that is the rule. But the model it copied can CEASE TO EXIST, and a
|
|
99
|
+
* conversation holding one does not fail cleanly: Pi runs the turn on its own default, so the
|
|
100
|
+
* picker names one model while another one answers. Measured before this change: removing the
|
|
101
|
+
* model in use left the picker empty (`source=unavailable, models=0`).
|
|
102
|
+
*/
|
|
103
|
+
async disable(providerId) {
|
|
104
|
+
const active = await this.registry.load();
|
|
105
|
+
const known = active.find((provider) => provider.id === providerId);
|
|
106
|
+
let models = known?.models ?? [];
|
|
107
|
+
if (!known) {
|
|
108
|
+
// A built-in has no `models.json` entry, so its list comes from Pi's catalogue - and it has to
|
|
109
|
+
// be captured NOW: once the credential is gone the catalogue no longer offers it, and the
|
|
110
|
+
// "off" card would have nothing to say about what switching it back on brings.
|
|
111
|
+
try {
|
|
112
|
+
const catalog = await this.runtime.catalog();
|
|
113
|
+
if (catalog.source === "pi") {
|
|
114
|
+
models = catalog.models
|
|
115
|
+
.filter((model) => model.provider === providerId)
|
|
116
|
+
.map((model) => ({
|
|
117
|
+
id: model.modelId,
|
|
118
|
+
name: model.label,
|
|
119
|
+
reasoning: model.reasoning,
|
|
120
|
+
imageInput: model.imageInput,
|
|
121
|
+
contextWindow: model.contextWindow,
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Still switchable off with an unknown model list; the record just says less about it.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
await this.registry.disable(providerId, models);
|
|
130
|
+
await this.afterChange([{ provider: providerId }]);
|
|
131
|
+
return this.list();
|
|
132
|
+
}
|
|
133
|
+
async enable(providerId) {
|
|
134
|
+
await this.registry.enable(providerId);
|
|
135
|
+
await this.afterChange();
|
|
136
|
+
return this.list();
|
|
40
137
|
}
|
|
41
138
|
/**
|
|
42
139
|
* Adds a provider, or adds a model to one, or updates one.
|
|
@@ -99,19 +196,42 @@ export class ProviderService {
|
|
|
99
196
|
throw error;
|
|
100
197
|
}
|
|
101
198
|
await this.afterChange();
|
|
102
|
-
return
|
|
199
|
+
return this.list();
|
|
103
200
|
}
|
|
104
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Removes a provider for good.
|
|
203
|
+
*
|
|
204
|
+
* THREE KINDS, ONE VERB, because the panel is a view of Pi's model configuration and deleting a
|
|
205
|
+
* provider means the same thing in all three cases:
|
|
206
|
+
* - a provider in `models.json`: its entry and its key go (the original behaviour);
|
|
207
|
+
* - a BUILT-IN with only a credential: its models are Pi's, so the credential is the only thing
|
|
208
|
+
* there is to delete - and deleting it is exactly what makes Pi stop offering it;
|
|
209
|
+
* - one that is already switched off: the remembered copy goes, which is what makes "off"
|
|
210
|
+
* permanent instead of something that would come back months later.
|
|
211
|
+
*/
|
|
105
212
|
async remove(providerId) {
|
|
106
213
|
const previous = await this.registry.load();
|
|
107
214
|
const existing = previous.find((provider) => provider.id === providerId);
|
|
108
|
-
if (!existing)
|
|
109
|
-
|
|
215
|
+
if (!existing) {
|
|
216
|
+
const disabled = (await this.registry.loadDisabled()).some((entry) => entry.id === providerId);
|
|
217
|
+
if (disabled) {
|
|
218
|
+
await this.registry.forgetDisabled(providerId);
|
|
219
|
+
await this.afterChange();
|
|
220
|
+
return this.list();
|
|
221
|
+
}
|
|
222
|
+
const builtin = (await this.registry.loadCredentialOnly()).some((provider) => provider.id === providerId);
|
|
223
|
+
if (builtin) {
|
|
224
|
+
await this.registry.removeCredential(providerId);
|
|
225
|
+
await this.afterChange([{ provider: providerId }]);
|
|
226
|
+
return this.list();
|
|
227
|
+
}
|
|
228
|
+
throw new Error(`Pi 的配置里没有供应商「${providerId}」`);
|
|
229
|
+
}
|
|
110
230
|
const next = previous.filter((provider) => provider.id !== providerId);
|
|
111
231
|
await this.registry.applyToPi(previous, next);
|
|
112
232
|
await this.registry.save(next);
|
|
113
|
-
await this.afterChange();
|
|
114
|
-
return
|
|
233
|
+
await this.afterChange([{ provider: providerId }]);
|
|
234
|
+
return this.list();
|
|
115
235
|
}
|
|
116
236
|
/**
|
|
117
237
|
* Removes ONE model from a provider, leaving the provider and its other models alone.
|
|
@@ -139,8 +259,8 @@ export class ProviderService {
|
|
|
139
259
|
const next = previous.map((provider) => (provider.id === providerId ? { ...provider, models: remaining } : provider));
|
|
140
260
|
await this.registry.applyToPi(previous, next);
|
|
141
261
|
await this.registry.save(next);
|
|
142
|
-
await this.afterChange();
|
|
143
|
-
return
|
|
262
|
+
await this.afterChange([{ provider: providerId, modelId }]);
|
|
263
|
+
return this.list();
|
|
144
264
|
}
|
|
145
265
|
/**
|
|
146
266
|
* What has to happen after the files are written, for the change to be REAL rather than pending.
|
|
@@ -150,9 +270,10 @@ export class ProviderService {
|
|
|
150
270
|
* UNFILTERED one, and handing that to a conversation that has model rules would offer it models
|
|
151
271
|
* those rules forbid.
|
|
152
272
|
*/
|
|
153
|
-
async afterChange() {
|
|
273
|
+
async afterChange(removed = []) {
|
|
154
274
|
this.runtime.invalidateCatalog?.();
|
|
155
275
|
this.runtime.bumpModelEpoch?.();
|
|
276
|
+
await this.repairConversationModels(removed);
|
|
156
277
|
for (const conversation of this.store.snapshot().conversations) {
|
|
157
278
|
if (conversation.archivedAt)
|
|
158
279
|
continue;
|
|
@@ -169,6 +290,40 @@ export class ProviderService {
|
|
|
169
290
|
await this.catalogService.broadcastAllCatalog?.();
|
|
170
291
|
this.broadcaster.broadcast({ type: "providers.list", providers: await this.list(), apis: [...KNOWN_APIS] });
|
|
171
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Drops a conversation's model when the change removed it.
|
|
295
|
+
*
|
|
296
|
+
* WHY THIS EXISTS AT ALL. A conversation whose model was just deleted is not merely stale - it is
|
|
297
|
+
* WRONG in a way that never surfaces: Pi cannot resolve the model and runs the turn on its own
|
|
298
|
+
* default, so the picker says one thing while another model answers. Clearing the field is the
|
|
299
|
+
* only honest state: the conversation follows Pi's default again, which is what the next turn
|
|
300
|
+
* does anyway.
|
|
301
|
+
*
|
|
302
|
+
* THE LIST IS PASSED IN, NOT DERIVED FROM A CATALOG READ. The caller knows exactly which models it
|
|
303
|
+
* just took away (one model, or a whole provider), so asking Pi what it offers would be a slower
|
|
304
|
+
* and less precise way to learn the same thing - and on a machine where Pi is slow or broken, the
|
|
305
|
+
* broadcast every open tab is waiting for would wait with it.
|
|
306
|
+
*
|
|
307
|
+
* ARCHIVED CONVERSATIONS ARE LEFT ALONE: they are read-only by design, and rewriting a frozen
|
|
308
|
+
* record's stored model would make the archive describe a past that did not happen.
|
|
309
|
+
*/
|
|
310
|
+
async repairConversationModels(removed) {
|
|
311
|
+
if (!removed.length)
|
|
312
|
+
return;
|
|
313
|
+
const gone = new Set(removed.map((item) => `${item.provider}/${item.modelId ?? "*"}`));
|
|
314
|
+
let changed = false;
|
|
315
|
+
for (const conversation of this.store.snapshot().conversations) {
|
|
316
|
+
const model = conversation.model;
|
|
317
|
+
if (conversation.archivedAt || !model)
|
|
318
|
+
continue;
|
|
319
|
+
if (!gone.has(`${model.provider}/${model.modelId}`) && !gone.has(`${model.provider}/*`))
|
|
320
|
+
continue;
|
|
321
|
+
await this.store.clearConversationModel(conversation.id);
|
|
322
|
+
changed = true;
|
|
323
|
+
}
|
|
324
|
+
if (changed)
|
|
325
|
+
this.broadcaster.broadcastState();
|
|
326
|
+
}
|
|
172
327
|
/**
|
|
173
328
|
* Discovers available models from the remote provider endpoint.
|
|
174
329
|
* If apiKey is omitted but providerId is given, uses the stored key.
|
|
@@ -1018,6 +1018,9 @@ export class RunService {
|
|
|
1018
1018
|
*/
|
|
1019
1019
|
const onSteerDelivered = (text) => {
|
|
1020
1020
|
enqueueTools("a queued message being delivered", async () => {
|
|
1021
|
+
// A delayed SDK delivery during abort still belongs to the stopped producer.
|
|
1022
|
+
if (controller.signal.aborted)
|
|
1023
|
+
return toolCalls;
|
|
1021
1024
|
const previous = assistant;
|
|
1022
1025
|
// Checkpoint first: the message being left behind is complete - Pi drains its queues at a turn
|
|
1023
1026
|
// boundary - and this is the last chance to write it durably with the lane it belongs to.
|
|
@@ -1039,7 +1042,7 @@ export class RunService {
|
|
|
1039
1042
|
// server never handed it - the prompt that opened the turn is the obvious case - and inventing an
|
|
1040
1043
|
// assistant message for one of those would open an empty bubble before every single turn.
|
|
1041
1044
|
if (!delivered)
|
|
1042
|
-
return
|
|
1045
|
+
return toolCalls;
|
|
1043
1046
|
const next = delivered.assistant;
|
|
1044
1047
|
this.broadcaster.broadcastState();
|
|
1045
1048
|
assistant = next;
|
|
@@ -1053,6 +1056,7 @@ export class RunService {
|
|
|
1053
1056
|
toolCalls = [];
|
|
1054
1057
|
return [];
|
|
1055
1058
|
});
|
|
1059
|
+
return pendingTools;
|
|
1056
1060
|
};
|
|
1057
1061
|
try {
|
|
1058
1062
|
for await (const delta of this.runtime.answer({
|
|
@@ -1351,10 +1355,10 @@ export class RunService {
|
|
|
1351
1355
|
if (error?.__superPiPreflight) {
|
|
1352
1356
|
throw error;
|
|
1353
1357
|
}
|
|
1354
|
-
await this.store.setStatus(conversation.id, "failed");
|
|
1355
|
-
if (
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
+
await this.store.setStatus(conversation.id, controller.signal.aborted ? "stopped" : "failed");
|
|
1359
|
+
if (controller.signal.aborted)
|
|
1360
|
+
return;
|
|
1361
|
+
await this.store.setFailureReason(conversation.id, formatTurnFailure(error));
|
|
1358
1362
|
throw error;
|
|
1359
1363
|
}
|
|
1360
1364
|
finally {
|
|
@@ -1424,6 +1428,8 @@ export class RunService {
|
|
|
1424
1428
|
throw new Error("这条消息已不在等待队列中,无法打断并回答");
|
|
1425
1429
|
this.interruptTargets.set(conversationId, messageId);
|
|
1426
1430
|
try {
|
|
1431
|
+
// Clear BEFORE abort: Pi can drain follow-ups while the old turn is winding down.
|
|
1432
|
+
await this.clearPiQueues(conversationId);
|
|
1427
1433
|
if (this.activeRuns.has(conversationId))
|
|
1428
1434
|
await this.stopRun(conversationId);
|
|
1429
1435
|
// PI'S QUEUES ARE EMPTIED BEFORE THE SELECTED MESSAGE IS ANSWERED ON ITS OWN. The message may be
|
|
@@ -1483,8 +1489,9 @@ export class RunService {
|
|
|
1483
1489
|
}
|
|
1484
1490
|
catch (error) {
|
|
1485
1491
|
await this.store.setStatus(conversationId, controller.signal.aborted ? "stopped" : "failed");
|
|
1486
|
-
if (
|
|
1487
|
-
|
|
1492
|
+
if (controller.signal.aborted)
|
|
1493
|
+
return;
|
|
1494
|
+
await this.store.setFailureReason(conversationId, formatTurnFailure(error));
|
|
1488
1495
|
throw error;
|
|
1489
1496
|
}
|
|
1490
1497
|
finally {
|
|
@@ -65,6 +65,11 @@ export class CommandHandler {
|
|
|
65
65
|
this.conversationService.releaseRevert(conversationId);
|
|
66
66
|
}
|
|
67
67
|
async handleCommand(command, sendToSocket, remoteAddress) {
|
|
68
|
+
if (command.type === "terminal.shell.set") {
|
|
69
|
+
if (!this.terminalService || !(await this.terminalService.setShell(command.command, remoteAddress)))
|
|
70
|
+
return;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
68
73
|
if (command.type === "terminal.open" || command.type === "terminal.input" || command.type === "terminal.resize" || command.type === "terminal.close") {
|
|
69
74
|
if (!this.terminalService)
|
|
70
75
|
return;
|
|
@@ -101,6 +106,22 @@ export class CommandHandler {
|
|
|
101
106
|
await this.providerService.removeModel(command.providerId, command.modelId);
|
|
102
107
|
return;
|
|
103
108
|
}
|
|
109
|
+
// OFF AND ON. Separate from `providers.remove` because removing is FOREVER - it drops the key -
|
|
110
|
+
// while switching a provider off keeps everything needed to switch it back on. The lapsed
|
|
111
|
+
// subscription is the case this exists for: the provider has to leave the picker, and the person
|
|
112
|
+
// may well renew the plan next month.
|
|
113
|
+
if (command.type === "providers.disable") {
|
|
114
|
+
if (!this.providerService)
|
|
115
|
+
throw new Error("供应商功能不可用");
|
|
116
|
+
await this.providerService.disable(command.providerId);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (command.type === "providers.enable") {
|
|
120
|
+
if (!this.providerService)
|
|
121
|
+
throw new Error("供应商功能不可用");
|
|
122
|
+
await this.providerService.enable(command.providerId);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
104
125
|
if (command.type === "providers.fetch") {
|
|
105
126
|
if (!this.providerService)
|
|
106
127
|
throw new Error("供应商功能不可用");
|
|
@@ -786,6 +786,14 @@ export function createHttpServer(options) {
|
|
|
786
786
|
.end(JSON.stringify({ enabled, reason: enabled ? "" : (context.terminalDisabledReason?.() ?? "终端未启用。") }));
|
|
787
787
|
return;
|
|
788
788
|
}
|
|
789
|
+
if (url.pathname === "/api/terminal-shell") {
|
|
790
|
+
const { defaultShell, shellSource, terminalShellCandidates } = await import("./terminal.js");
|
|
791
|
+
const configured = context.store.snapshot().terminalShell;
|
|
792
|
+
const shell = defaultShell(process.env, configured);
|
|
793
|
+
response.writeHead(200, { "content-type": "application/json; charset=utf-8" })
|
|
794
|
+
.end(JSON.stringify({ shell, source: shellSource(process.env, configured), candidates: terminalShellCandidates() }));
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
789
797
|
if (url.pathname === "/api/git") {
|
|
790
798
|
const json = (status, body) => response.writeHead(status, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify(body));
|
|
791
799
|
try {
|
package/dist/server/index.js
CHANGED
|
@@ -237,6 +237,8 @@ const workspaceService = new WorkspaceService(store, runtime, broadcaster, catal
|
|
|
237
237
|
const terminalService = new TerminalService({
|
|
238
238
|
workspaceFor: (workspaceId) => store.snapshot().workspaces.find((workspace) => workspace.id === workspaceId),
|
|
239
239
|
isTrustedPeer: (remoteAddress) => isLocalOrPrivateAddress(remoteAddress),
|
|
240
|
+
configuredShell: () => store.snapshot().terminalShell,
|
|
241
|
+
saveConfiguredShell: async (shell) => { await store.setTerminalShell(shell); broadcaster.broadcastState(); },
|
|
240
242
|
});
|
|
241
243
|
/**
|
|
242
244
|
* Background command execution, independent from subagents. Uses node-pty to run shell commands
|
|
@@ -48,6 +48,11 @@ export type TurnItem =
|
|
|
48
48
|
kind: "text";
|
|
49
49
|
delta: string;
|
|
50
50
|
}
|
|
51
|
+
/** Non-text notices share the text FIFO; delivery may await a transcript boundary. */
|
|
52
|
+
| {
|
|
53
|
+
kind: "callback";
|
|
54
|
+
run: () => void | Promise<void>;
|
|
55
|
+
}
|
|
51
56
|
/** The turn is over. */
|
|
52
57
|
| {
|
|
53
58
|
kind: "end";
|
|
@@ -75,7 +80,7 @@ export interface TurnEventHandlers {
|
|
|
75
80
|
onThinking?(delta: string): void;
|
|
76
81
|
onThinkingEnd?(): void;
|
|
77
82
|
/** A steer this turn delivered - the text Pi picked up from its own queue. */
|
|
78
|
-
onSteerDelivered?(text: string): void
|
|
83
|
+
onSteerDelivered?(text: string): void | Promise<void>;
|
|
79
84
|
/** Pi's own queue, as a full snapshot. */
|
|
80
85
|
onPiQueue?(queue: {
|
|
81
86
|
steering: string[];
|
|
@@ -64,6 +64,54 @@ export type ProviderDefinition = {
|
|
|
64
64
|
apiKey?: string;
|
|
65
65
|
hasAuthKey?: boolean;
|
|
66
66
|
models: ProviderModelDefinition[];
|
|
67
|
+
/**
|
|
68
|
+
* The key is written into `models.json` itself rather than into `auth.json`.
|
|
69
|
+
*
|
|
70
|
+
* Pi's own documentation leads with it (`"apiKey": "$MY_KEY"`), and it is the only way to point a
|
|
71
|
+
* provider at a key that lives in the environment. It matters here because such a provider IS
|
|
72
|
+
* offered by Pi when the variable is set - so the panel must not tell the person it has no
|
|
73
|
+
* credential, and must not warn that Pi will not offer it.
|
|
74
|
+
*/
|
|
75
|
+
keyInModelsJson?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* TRUE when the provider is one of Pi's own, with no entry in `models.json`.
|
|
78
|
+
*
|
|
79
|
+
* Its models come from Pi itself and the only thing a person has configured for it is the
|
|
80
|
+
* credential, so it can be switched off but has no address to edit.
|
|
81
|
+
*/
|
|
82
|
+
builtin?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* TRUE when the provider is switched OFF: Pi does not offer it, and switching it back on restores
|
|
85
|
+
* exactly what was there.
|
|
86
|
+
*/
|
|
87
|
+
disabled?: boolean;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* A provider that was switched OFF, and everything needed to switch it back on.
|
|
91
|
+
*
|
|
92
|
+
* WHY THIS EXISTS AT ALL. Turning a provider off means taking it out of Pi's files - the
|
|
93
|
+
* `models.json` entry and the `auth.json` credential - because that is the only thing Pi honours
|
|
94
|
+
* (a provider is offered exactly when it has a working credential, and `ModelRuntime.getAvailable()`
|
|
95
|
+
* is what the picker reads). Taking something out of a file is not the same as throwing it away, so
|
|
96
|
+
* what was taken out is written down HERE, in Super Pi's own directory, with the same restrictive
|
|
97
|
+
* permissions the registry already has. Switching it back on is then a matter of putting those two
|
|
98
|
+
* pieces back where Pi reads them.
|
|
99
|
+
*
|
|
100
|
+
* THE CREDENTIAL IS IN HERE, and that is deliberate. A provider that is switched off is one a person
|
|
101
|
+
* may well switch back on - a coding plan that was renewed, a proxy that was moved - and a
|
|
102
|
+
* credential that cannot be recovered cannot be restored. The file is 0600, it lives in Super Pi's own
|
|
103
|
+
* data directory, and nothing in it is ever broadcast: the browser is told only that a key exists.
|
|
104
|
+
*/
|
|
105
|
+
export type DisabledProvider = {
|
|
106
|
+
id: string;
|
|
107
|
+
/** The provider's name, as it was. */
|
|
108
|
+
name?: string;
|
|
109
|
+
/** The `models.json` entry, verbatim, when the provider had one. */
|
|
110
|
+
modelsEntry?: Record<string, unknown>;
|
|
111
|
+
/** The `auth.json` entry, verbatim, when the provider had a credential. */
|
|
112
|
+
credential?: Record<string, unknown>;
|
|
113
|
+
/** The models the provider offered, so the panel can show what switching it back on brings. */
|
|
114
|
+
models: ProviderModelDefinition[];
|
|
67
115
|
};
|
|
68
116
|
/** One model, as the browser sees it. No key, no nothing secret - there is nothing secret here. */
|
|
69
117
|
export type { PublicProvider, PublicProviderModel } from "./types.js";
|
|
@@ -116,11 +164,69 @@ export declare class ProviderRegistry {
|
|
|
116
164
|
constructor(dataDir: string, piDir?: string);
|
|
117
165
|
/** Super Pi's own registry: the providers it manages, with their keys. */
|
|
118
166
|
private get registryPath();
|
|
167
|
+
/** The registry file as one object: the managed providers AND the ones turned off. */
|
|
168
|
+
private readRegistry;
|
|
169
|
+
/** Writes one half of the registry file without losing the other. */
|
|
170
|
+
private writeRegistry;
|
|
119
171
|
/**
|
|
120
172
|
* Reads all configured providers directly from Pi's models.json and auth.json.
|
|
121
173
|
* Super Pi acts as the direct visual management plane for Pi's model configurations.
|
|
174
|
+
*
|
|
175
|
+
* PROVIDERS THAT ARE TURNED OFF ARE NOT IN HERE. They have been taken out of Pi's files - that is
|
|
176
|
+
* what "off" means - so this list is exactly what Pi would offer, which is what the write path
|
|
177
|
+
* (`set`, `remove`, `applyToPi`) has to reason about. `loadDisabled()` is the other half, for the
|
|
178
|
+
* panel that has to be able to switch them back on.
|
|
122
179
|
*/
|
|
123
180
|
load(): Promise<ProviderDefinition[]>;
|
|
181
|
+
/**
|
|
182
|
+
* Pi's OWN providers: the ones that exist because they have a credential in `auth.json` and no
|
|
183
|
+
* entry in `models.json`.
|
|
184
|
+
*
|
|
185
|
+
* WHY THIS IS A SEPARATE READ. A built-in such as `kimi-coding` is defined inside Pi, not in a
|
|
186
|
+
* file - the only thing a person has configured for it is the credential. Those providers are
|
|
187
|
+
* invisible to `load()`, which reads `models.json`, and that is correct for the WRITE path:
|
|
188
|
+
* writing an empty `models.json` entry for a built-in would shadow Pi's own definition with a
|
|
189
|
+
* broken one. But they are exactly what a person wants to switch off ("I stopped paying for this
|
|
190
|
+
* plan"), so the panel has to see them. This returns the ids and the fact of a credential; the
|
|
191
|
+
* models come from Pi's catalogue, which this file layer cannot see.
|
|
192
|
+
*/
|
|
193
|
+
loadCredentialOnly(): Promise<ProviderDefinition[]>;
|
|
194
|
+
/** The providers that are switched off, as the panel needs to see them. */
|
|
195
|
+
loadDisabled(): Promise<DisabledProvider[]>;
|
|
196
|
+
/**
|
|
197
|
+
* Takes a provider out of Pi's configuration and remembers exactly what was taken out.
|
|
198
|
+
*
|
|
199
|
+
* TWO FILES, and both are needed. `models.json` is where a custom provider's models are defined;
|
|
200
|
+
* `auth.json` is where its credential is. Pi offers a provider's models only when its auth is
|
|
201
|
+
* configured (`ModelRuntime.getAvailable()` filters on exactly that), so removing the credential
|
|
202
|
+
* is what makes a BUILT-IN stop being offered - it has no `models.json` entry to remove - while
|
|
203
|
+
* removing both is what makes a custom one disappear completely.
|
|
204
|
+
*
|
|
205
|
+
* NOTHING IS THROWN AWAY. Both pieces are written into Super Pi's own registry, which is how
|
|
206
|
+
* `enable` can put them back byte for byte. A person switching off a lapsed subscription provider
|
|
207
|
+
* is not asking to forget their endpoint and their models.
|
|
208
|
+
*/
|
|
209
|
+
disable(providerId: string, models?: ProviderModelDefinition[]): Promise<void>;
|
|
210
|
+
/**
|
|
211
|
+
* Puts a provider back exactly where `disable` took it from.
|
|
212
|
+
*
|
|
213
|
+
* AN EXISTING ENTRY IS NOT OVERWRITTEN. The person may have re-created the provider (or written a
|
|
214
|
+
* new `models.json` entry for the same id) while it was off; the newer entry is theirs and wins,
|
|
215
|
+
* and only the missing piece is restored. Refusing would be worse: the switch says "back on", and
|
|
216
|
+
* the credential is what makes it usable.
|
|
217
|
+
*/
|
|
218
|
+
enable(providerId: string): Promise<void>;
|
|
219
|
+
/**
|
|
220
|
+
* Deletes a BUILT-IN provider's credential from Pi's `auth.json`.
|
|
221
|
+
*
|
|
222
|
+
* THE ONLY THING THERE IS TO DELETE. A built-in's models live in Pi itself; what a person has
|
|
223
|
+
* configured for it is the credential, and removing that is what makes Pi stop offering it. This
|
|
224
|
+
* is the PERMANENT version: unlike `disable`, nothing is remembered, so switching it back on means
|
|
225
|
+
* logging in to that provider again (which is what re-subscribing to the plan involves anyway).
|
|
226
|
+
*/
|
|
227
|
+
removeCredential(providerId: string): Promise<void>;
|
|
228
|
+
/** Forgets a disabled provider without restoring anything: the permanent form of "off". */
|
|
229
|
+
forgetDisabled(providerId: string): Promise<void>;
|
|
124
230
|
/** Writes the registry. The key lives here, so the file is 0600. */
|
|
125
231
|
save(providers: ProviderDefinition[]): Promise<void>;
|
|
126
232
|
/**
|