@sangyaahub/pi-marg 1.0.7 → 1.0.8
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/.omp-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +6 -0
- package/MODEL-SETUP.md +1 -1
- package/README.md +2 -2
- package/adapters/omp/extensions/model-router.ts +28 -0
- package/adapters/omp/extensions/provider-setup.ts +27 -0
- package/adapters/pi/extensions/model-router.ts +39 -0
- package/core/model-routing.ts +163 -2
- package/core/provider-config.ts +22 -0
- package/package.json +1 -1
- package/scripts/install.sh +1 -1
- package/scripts/validate.sh +1 -1
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "PiMarg — a governed development path for Pi Agent and Oh My Pi.",
|
|
10
|
-
"version": "1.0.
|
|
10
|
+
"version": "1.0.8"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
14
14
|
"name": "pi-marg",
|
|
15
15
|
"description": "Intent-led development workflow with live A/B/C/D routing, capability detection, verification, and approval gates.",
|
|
16
16
|
"source": "./",
|
|
17
|
-
"version": "1.0.
|
|
17
|
+
"version": "1.0.8",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Sangyaa",
|
|
20
20
|
"email": "contact@sangyaa.co"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
|
+
## 1.0.8 — 2026-10-26
|
|
3
|
+
|
|
4
|
+
- A provider reporting 95% usage pauses while its stage context is kept. The saved high then low backup resumes the stage only when it is a different provider, fits the same stage, and is itself under 95%. Hard quota errors keep the existing failover.
|
|
5
|
+
- Provider lines in the catalog show remaining usage when the host reports `percentUsed`, `percentRemaining`, or `used`/`limit`, and say usage remaining is unknown otherwise.
|
|
6
|
+
- An empty OMP catalog can add the optional Kimi Code preset through the same custom-provider writer. It stores the `KIMI_API_KEY` env var name, writes no API key, and is not needed for a logged-in (OAuth) `kimi-code` provider.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
## 1.0.7 — 2026-10-03
|
|
4
10
|
|
package/MODEL-SETUP.md
CHANGED
|
@@ -16,7 +16,7 @@ LIVE AUTHENTICATED CATALOG
|
|
|
16
16
|
high backup ──► low backup
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
If the live catalog is empty, `/pi-marg` offers
|
|
19
|
+
If the live catalog is empty, `/pi-marg` offers four actions: sign in with `/login`, add a custom OpenAI-compatible provider, add the optional Kimi Code preset, or cancel. The custom path writes `apiKey: ENV_VAR_NAME` into `~/.omp/agent/models.yml` and the secret only into `~/.omp/agent/.env`. The Kimi preset uses that same writer with provider id `kimi-code`, display name `Kimi Code`, base URL `https://api.kimi.com/coding/v1`, env var `KIMI_API_KEY`, and the model ids from OMP's Kimi Code provider rule. It does not write an API key. Cancelling writes neither file. A logged-in Kimi provider is listed with every other authenticated provider; the preset is not required for that.
|
|
20
20
|
|
|
21
21
|
## Roles
|
|
22
22
|
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<a href="https://github.com/sangyaahub/pi-marg/releases"><img alt="Version 1.0.
|
|
6
|
+
<a href="https://github.com/sangyaahub/pi-marg/releases"><img alt="Version 1.0.8" src="https://img.shields.io/badge/version-1.0.8-0451F7"></a>
|
|
7
7
|
<a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-C27B1A"></a>
|
|
8
8
|
<a href="https://pi.dev/packages?name=pi-marg"><img alt="Pi package" src="https://img.shields.io/badge/Pi-package-0338A8"></a>
|
|
9
9
|
<a href="https://omp.sh"><img alt="Oh My Pi plugin" src="https://img.shields.io/badge/OMP-plugin-021E5C"></a>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
# PiMarg
|
|
13
13
|
|
|
14
|
-
**Version 1.0.
|
|
14
|
+
**Version 1.0.8 · MIT licensed · Pi Agent + Oh My Pi · by [Sangyaa](https://sangyaa.co)**
|
|
15
15
|
|
|
16
16
|
PiMarg turns an ordinary prompt into a governed development path. It identifies the work boundary, selects one engineering method, discovers the models and tools the active runtime really provides, separates thinking from execution, records evidence, and stops before protected actions.
|
|
17
17
|
|
|
@@ -10,8 +10,13 @@ import {
|
|
|
10
10
|
executeAutomaticFailover,
|
|
11
11
|
executeModelRoute,
|
|
12
12
|
failedModelSelector,
|
|
13
|
+
chooseUsageResume,
|
|
14
|
+
executeUsageResume,
|
|
13
15
|
findTerminalUsageLimitFailure,
|
|
14
16
|
formatFailoverRecoveryMessage,
|
|
17
|
+
formatUsagePauseMessage,
|
|
18
|
+
providerReachedUsagePause,
|
|
19
|
+
usageSnapshotsFromMessages,
|
|
15
20
|
hasConfiguredFallback,
|
|
16
21
|
modelFailureKey,
|
|
17
22
|
modelToolUsageLimitFailure,
|
|
@@ -370,6 +375,29 @@ export default function modelRouter(pi: any) {
|
|
|
370
375
|
|
|
371
376
|
pi.on("agent_end", async (event: any, ctx: any) => {
|
|
372
377
|
if (event?.willContinue || event?.isTerminal === false) return;
|
|
378
|
+
const liveModels = (ctx.models?.list?.() ?? []) as ModelLike[];
|
|
379
|
+
const current = ctx.models?.current?.() ?? ctx.model;
|
|
380
|
+
const snapshots = usageSnapshotsFromMessages(event?.messages ?? [], liveModels);
|
|
381
|
+
const pause = typeof current?.provider === "string" ? providerReachedUsagePause(snapshots, current.provider) : undefined;
|
|
382
|
+
if (pause && typeof current?.id === "string" && hasConfiguredFallback(state)) {
|
|
383
|
+
const pausedSelector = `${current.provider}/${current.id}`;
|
|
384
|
+
const resume = chooseUsageResume(state, snapshots, current.provider);
|
|
385
|
+
if (resume) {
|
|
386
|
+
const execution = await executeUsageResume(state, pausedSelector, resume, {
|
|
387
|
+
runtimeName: "OMP",
|
|
388
|
+
models: liveModels,
|
|
389
|
+
hasExternalDevin: hasExternalDevin(pi),
|
|
390
|
+
activate: (model) => pi.setModel(model),
|
|
391
|
+
persist: (nextState) => persistRoute(nextState, ctx),
|
|
392
|
+
});
|
|
393
|
+
state = execution.state;
|
|
394
|
+
if (execution.switched) {
|
|
395
|
+
ctx.ui?.notify?.(`PiMarg paused ${pausedSelector} at ${pause.percentUsed}% usage and resumed on ${execution.switched.selection.selector}.`, "warning");
|
|
396
|
+
pi.sendUserMessage(formatUsagePauseMessage(pausedSelector, pause, execution.switched.selection.selector), { deliverAs: "aside" });
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
373
401
|
const failed = findTerminalUsageLimitFailure(event?.messages ?? []);
|
|
374
402
|
const failure = failed
|
|
375
403
|
? {
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
kimiCodeProviderPreset,
|
|
5
6
|
probeModelIds,
|
|
6
7
|
validateCustomProvider,
|
|
7
8
|
writeCustomProvider,
|
|
@@ -17,6 +18,7 @@ import { agentDirectory } from "../../../core/user-route";
|
|
|
17
18
|
const SETUP_ITEMS = [
|
|
18
19
|
"Sign in to a provider",
|
|
19
20
|
"Add a custom OpenAI-compatible provider",
|
|
21
|
+
"Add the Kimi Code preset",
|
|
20
22
|
"Cancel",
|
|
21
23
|
] as const;
|
|
22
24
|
|
|
@@ -157,9 +159,34 @@ export async function addCustomProvider(ctx: any, home: string | undefined): Pro
|
|
|
157
159
|
return { status: "ready", models };
|
|
158
160
|
}
|
|
159
161
|
|
|
162
|
+
export async function addKimiCodePreset(ctx: any, home: string | undefined): Promise<CatalogSetup> {
|
|
163
|
+
const preset = kimiCodeProviderPreset();
|
|
164
|
+
try {
|
|
165
|
+
const directory = agentDirectory("OMP", home);
|
|
166
|
+
writeCustomProvider({
|
|
167
|
+
modelsPath: join(directory, "models.yml"),
|
|
168
|
+
envPath: join(directory, ".env"),
|
|
169
|
+
}, preset);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
ctx.ui.notify(error instanceof Error ? error.message : "Could not write the Kimi Code preset.", "error");
|
|
172
|
+
return { status: "cancelled" };
|
|
173
|
+
}
|
|
174
|
+
ctx.ui.notify(
|
|
175
|
+
`Kimi Code was added to models.yml. Set ${preset.envVar} in the environment; the API key was not stored.`,
|
|
176
|
+
"info",
|
|
177
|
+
);
|
|
178
|
+
if (typeof ctx.models?.reload === "function") {
|
|
179
|
+
await ctx.models.reload();
|
|
180
|
+
}
|
|
181
|
+
const models = (ctx.models.list() ?? []) as ModelLike[];
|
|
182
|
+
if (models.length === 0) return { status: "restart" };
|
|
183
|
+
return { status: "ready", models };
|
|
184
|
+
}
|
|
185
|
+
|
|
160
186
|
export async function configureEmptyCatalog(ctx: any, home: string | undefined): Promise<CatalogSetup> {
|
|
161
187
|
const choice = await askItems(ctx, "Model setup", SETUP_ITEMS);
|
|
162
188
|
if (!choice || choice === "Cancel") return { status: "cancelled" };
|
|
189
|
+
if (choice === "Add the Kimi Code preset") return addKimiCodePreset(ctx, home);
|
|
163
190
|
if (choice === "Sign in to a provider") {
|
|
164
191
|
if (typeof ctx.openLogin === "function") await ctx.openLogin();
|
|
165
192
|
else ctx.ui.notify("Sign in with /login, verify with `omp models`, then run /pi-marg again.", "warning");
|
|
@@ -7,13 +7,20 @@ import {
|
|
|
7
7
|
executeAutomaticFailover,
|
|
8
8
|
executeModelRoute,
|
|
9
9
|
failedModelSelector,
|
|
10
|
+
chooseUsageResume,
|
|
11
|
+
executeUsageResume,
|
|
10
12
|
findTerminalUsageLimitFailure,
|
|
11
13
|
formatFailoverRecoveryMessage,
|
|
14
|
+
formatUsagePauseMessage,
|
|
15
|
+
providerReachedUsagePause,
|
|
16
|
+
usageSnapshotsFromMessages,
|
|
12
17
|
hasConfiguredFallback,
|
|
13
18
|
modelFailureKey,
|
|
14
19
|
modelToolUsageLimitFailure,
|
|
15
20
|
type ModelLike,
|
|
16
21
|
type ModelRouteParams,
|
|
22
|
+
type ModelSelection,
|
|
23
|
+
type ProviderUsageSnapshot,
|
|
17
24
|
} from "../../../core/model-routing";
|
|
18
25
|
import { loadRuntimeRoute, rememberRuntimeRoute } from "../../../core/user-route";
|
|
19
26
|
|
|
@@ -36,6 +43,7 @@ export default function modelRouter(pi: any) {
|
|
|
36
43
|
let state = emptyModelRouteState();
|
|
37
44
|
let recoveryToken: symbol | undefined;
|
|
38
45
|
let sessionGeneration = 0;
|
|
46
|
+
let pendingUsagePause: { selector: string; snapshot: ProviderUsageSnapshot; resume: ModelSelection } | undefined;
|
|
39
47
|
let pendingModelFailure: { selector: string; key: string } | undefined;
|
|
40
48
|
let pendingToolFailure: { selector: string; key: string } | undefined;
|
|
41
49
|
const routeHome = (ctx: any) => typeof ctx?.agentHome === "string" ? ctx.agentHome : undefined;
|
|
@@ -50,6 +58,7 @@ export default function modelRouter(pi: any) {
|
|
|
50
58
|
const restore = (_event: unknown, ctx: any) => {
|
|
51
59
|
sessionGeneration += 1;
|
|
52
60
|
recoveryToken = undefined;
|
|
61
|
+
pendingUsagePause = undefined;
|
|
53
62
|
pendingModelFailure = undefined;
|
|
54
63
|
pendingToolFailure = undefined;
|
|
55
64
|
state = loadRuntimeRoute("Pi", ctx.sessionManager.getBranch(), routeHome(ctx));
|
|
@@ -128,6 +137,17 @@ export default function modelRouter(pi: any) {
|
|
|
128
137
|
};
|
|
129
138
|
|
|
130
139
|
pi.on("agent_end", (event: any, ctx: any) => {
|
|
140
|
+
const liveModels = availableModels(ctx);
|
|
141
|
+
const current = ctx.model as ModelLike | undefined;
|
|
142
|
+
const snapshots = usageSnapshotsFromMessages(event?.messages ?? [], liveModels);
|
|
143
|
+
const pause = current?.provider ? providerReachedUsagePause(snapshots, current.provider) : undefined;
|
|
144
|
+
if (pause && current?.id && hasConfiguredFallback(state)) {
|
|
145
|
+
const pausedSelector = `${current.provider}/${current.id}`;
|
|
146
|
+
const resume = chooseUsageResume(state, snapshots, current.provider);
|
|
147
|
+
if (resume) {
|
|
148
|
+
pendingUsagePause = { selector: pausedSelector, snapshot: pause, resume };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
131
151
|
const failed = findTerminalUsageLimitFailure(event?.messages ?? []);
|
|
132
152
|
pendingModelFailure = failed
|
|
133
153
|
? { selector: failedModelSelector(failed, ctx.model), key: modelFailureKey(failed) }
|
|
@@ -139,6 +159,25 @@ export default function modelRouter(pi: any) {
|
|
|
139
159
|
});
|
|
140
160
|
|
|
141
161
|
pi.on("agent_settled", async (_event: unknown, ctx: any) => {
|
|
162
|
+
const usagePause = pendingUsagePause;
|
|
163
|
+
pendingUsagePause = undefined;
|
|
164
|
+
if (usagePause && !pendingModelFailure && !pendingToolFailure) {
|
|
165
|
+
const execution = await executeUsageResume(state, usagePause.selector, usagePause.resume, {
|
|
166
|
+
runtimeName: "Pi",
|
|
167
|
+
models: availableModels(ctx),
|
|
168
|
+
hasExternalDevin: hasExternalDevin(pi),
|
|
169
|
+
activate: (model) => pi.setModel(model),
|
|
170
|
+
persist: (nextState) => persistRoute(nextState, ctx),
|
|
171
|
+
});
|
|
172
|
+
state = execution.state;
|
|
173
|
+
if (execution.switched) {
|
|
174
|
+
ctx.ui?.notify?.(`PiMarg paused ${usagePause.selector} at ${usagePause.snapshot.percentUsed}% usage and resumed on ${execution.switched.selection.selector}.`, "warning");
|
|
175
|
+
pi.sendUserMessage(formatUsagePauseMessage(usagePause.selector, usagePause.snapshot, execution.switched.selection.selector), { deliverAs: "followUp" });
|
|
176
|
+
pendingModelFailure = undefined;
|
|
177
|
+
pendingToolFailure = undefined;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
142
181
|
const failure = pendingModelFailure ?? pendingToolFailure;
|
|
143
182
|
pendingModelFailure = undefined;
|
|
144
183
|
pendingToolFailure = undefined;
|
package/core/model-routing.ts
CHANGED
|
@@ -3,13 +3,29 @@ export type FallbackLevel = "high" | "low";
|
|
|
3
3
|
export type CandidateTier = "frontier" | "execution" | "general";
|
|
4
4
|
export type RuntimeName = "OMP" | "Pi";
|
|
5
5
|
|
|
6
|
+
export interface ModelUsage {
|
|
7
|
+
percentUsed?: number;
|
|
8
|
+
percentRemaining?: number;
|
|
9
|
+
used?: number;
|
|
10
|
+
limit?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export interface ModelLike {
|
|
7
14
|
provider: string;
|
|
8
15
|
id: string;
|
|
9
16
|
name?: string;
|
|
10
17
|
cost?: Record<string, unknown>;
|
|
18
|
+
usage?: ModelUsage;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ProviderUsageSnapshot {
|
|
22
|
+
provider: string;
|
|
23
|
+
percentUsed: number;
|
|
24
|
+
percentRemaining: number;
|
|
11
25
|
}
|
|
12
26
|
|
|
27
|
+
export const USAGE_PAUSE_PERCENT = 95;
|
|
28
|
+
|
|
13
29
|
export interface ModelCandidate {
|
|
14
30
|
selector: string;
|
|
15
31
|
identity: string;
|
|
@@ -158,6 +174,108 @@ export function emptyModelRouteState(): ModelRouteState {
|
|
|
158
174
|
return { version: 2, selections: {}, fallbacks: {} };
|
|
159
175
|
}
|
|
160
176
|
|
|
177
|
+
|
|
178
|
+
function finitePercent(value: unknown): number | undefined {
|
|
179
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
180
|
+
return Math.min(100, Math.max(0, value));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function percentUsedFromUsage(usage: ModelUsage | undefined): number | undefined {
|
|
184
|
+
if (!usage) return undefined;
|
|
185
|
+
const used = finitePercent(usage.percentUsed);
|
|
186
|
+
if (used !== undefined) return used;
|
|
187
|
+
const remaining = finitePercent(usage.percentRemaining);
|
|
188
|
+
if (remaining !== undefined) return 100 - remaining;
|
|
189
|
+
if (typeof usage.used === "number" && typeof usage.limit === "number" && usage.limit > 0 && Number.isFinite(usage.used)) {
|
|
190
|
+
return finitePercent((usage.used / usage.limit) * 100);
|
|
191
|
+
}
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function providerUsageSnapshots(models: readonly ModelLike[]): ProviderUsageSnapshot[] {
|
|
196
|
+
const byProvider = new Map<string, number>();
|
|
197
|
+
for (const model of models) {
|
|
198
|
+
const percentUsed = percentUsedFromUsage(model.usage);
|
|
199
|
+
if (percentUsed === undefined) continue;
|
|
200
|
+
const current = byProvider.get(model.provider);
|
|
201
|
+
if (current === undefined || percentUsed > current) byProvider.set(model.provider, percentUsed);
|
|
202
|
+
}
|
|
203
|
+
return [...byProvider.entries()]
|
|
204
|
+
.map(([provider, percentUsed]) => ({
|
|
205
|
+
provider,
|
|
206
|
+
percentUsed,
|
|
207
|
+
percentRemaining: Math.max(0, 100 - percentUsed),
|
|
208
|
+
}))
|
|
209
|
+
.sort((left, right) => left.provider.localeCompare(right.provider));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function usageSnapshotsFromMessages(
|
|
213
|
+
messages: readonly unknown[],
|
|
214
|
+
models: readonly ModelLike[] = [],
|
|
215
|
+
): ProviderUsageSnapshot[] {
|
|
216
|
+
const reported: ModelLike[] = [...models];
|
|
217
|
+
for (const message of messages) {
|
|
218
|
+
const record = asRecord(message);
|
|
219
|
+
if (!record || typeof record.provider !== "string") continue;
|
|
220
|
+
const usage = asRecord(record.usage);
|
|
221
|
+
if (!usage) continue;
|
|
222
|
+
reported.push({
|
|
223
|
+
provider: record.provider,
|
|
224
|
+
id: typeof record.model === "string" ? record.model : "message",
|
|
225
|
+
usage: {
|
|
226
|
+
percentUsed: typeof usage.percentUsed === "number" ? usage.percentUsed : undefined,
|
|
227
|
+
percentRemaining: typeof usage.percentRemaining === "number" ? usage.percentRemaining : undefined,
|
|
228
|
+
used: typeof usage.used === "number" ? usage.used : undefined,
|
|
229
|
+
limit: typeof usage.limit === "number" ? usage.limit : undefined,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return providerUsageSnapshots(reported);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function providerReachedUsagePause(
|
|
237
|
+
snapshots: readonly ProviderUsageSnapshot[],
|
|
238
|
+
provider: string,
|
|
239
|
+
): ProviderUsageSnapshot | undefined {
|
|
240
|
+
return snapshots.find((snapshot) => snapshot.provider === provider && snapshot.percentUsed >= USAGE_PAUSE_PERCENT);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function fallbackFitsStage(stage: AutoStage | undefined, selection: ModelSelection): boolean {
|
|
244
|
+
if (!stage || selection.tier === "general") return true;
|
|
245
|
+
if (stage === "A" || stage === "C") return selection.tier === "frontier";
|
|
246
|
+
return selection.tier === "execution";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function chooseUsageResume(
|
|
250
|
+
state: ModelRouteState,
|
|
251
|
+
snapshots: readonly ProviderUsageSnapshot[],
|
|
252
|
+
pausedProvider: string,
|
|
253
|
+
): ModelSelection | undefined {
|
|
254
|
+
for (const level of FALLBACK_LEVELS) {
|
|
255
|
+
const selection = state.fallbacks[level];
|
|
256
|
+
if (!selection || selection.access !== "runtime-model") continue;
|
|
257
|
+
if (selection.provider === pausedProvider) continue;
|
|
258
|
+
if (!fallbackFitsStage(state.activeStage, selection)) continue;
|
|
259
|
+
const usage = snapshots.find((snapshot) => snapshot.provider === selection.provider);
|
|
260
|
+
if (usage && usage.percentUsed >= USAGE_PAUSE_PERCENT) continue;
|
|
261
|
+
return selection;
|
|
262
|
+
}
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function formatUsagePauseMessage(
|
|
267
|
+
pausedSelector: string,
|
|
268
|
+
snapshot: ProviderUsageSnapshot,
|
|
269
|
+
resumeSelector: string,
|
|
270
|
+
): string {
|
|
271
|
+
return [
|
|
272
|
+
`[PiMarg usage pause] ${pausedSelector} is at ${snapshot.percentUsed}% usage (${snapshot.percentRemaining}% remaining).`,
|
|
273
|
+
"Paused and kept the current stage context.",
|
|
274
|
+
`Resume on ${resumeSelector}, a saved backup of the same stage capability.`,
|
|
275
|
+
].join("\n");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
161
279
|
export function isFallbackLevel(value: unknown): value is FallbackLevel {
|
|
162
280
|
return typeof value === "string" && FALLBACK_LEVELS.includes(value as FallbackLevel);
|
|
163
281
|
}
|
|
@@ -471,6 +589,7 @@ export function formatCatalogPresentation(
|
|
|
471
589
|
catalog: Record<AutoStage, ModelCandidate[]>,
|
|
472
590
|
requiredStages: AutoStage[],
|
|
473
591
|
providerFilter?: string,
|
|
592
|
+
usage: readonly ProviderUsageSnapshot[] = [],
|
|
474
593
|
) {
|
|
475
594
|
const notes = catalogNotes(runtimeName, catalog);
|
|
476
595
|
const orderedStages = ascendingStages(requiredStages);
|
|
@@ -520,7 +639,9 @@ export function formatCatalogPresentation(
|
|
|
520
639
|
lines.push("Providers:");
|
|
521
640
|
for (const provider of providers) {
|
|
522
641
|
const marker = providerFilter === provider.name ? " ← selected" : "";
|
|
523
|
-
|
|
642
|
+
const snapshot = usage.find((item) => item.provider === provider.name);
|
|
643
|
+
const remaining = snapshot ? ` — ${snapshot.percentRemaining}% remaining` : " — usage remaining unknown";
|
|
644
|
+
lines.push(` ${provider.index}. ${provider.name} (${provider.count})${remaining}${marker}`);
|
|
524
645
|
}
|
|
525
646
|
if (!providerFilter) {
|
|
526
647
|
lines.push("Call catalog again with provider=<name> to list every model for that provider.");
|
|
@@ -583,6 +704,45 @@ export function restoreModelRouteState(entries: readonly any[]): ModelRouteState
|
|
|
583
704
|
return restored;
|
|
584
705
|
}
|
|
585
706
|
|
|
707
|
+
|
|
708
|
+
export async function executeUsageResume(
|
|
709
|
+
currentState: ModelRouteState,
|
|
710
|
+
pausedSelector: string,
|
|
711
|
+
selection: ModelSelection,
|
|
712
|
+
port: ModelRoutePort,
|
|
713
|
+
): Promise<AutomaticFailoverExecution> {
|
|
714
|
+
const level = (FALLBACK_LEVELS.find((item) => currentState.fallbacks[item]?.selector === selection.selector) ?? "high");
|
|
715
|
+
const model = port.models.find((item) => `${item.provider}/${item.id}` === selection.selector);
|
|
716
|
+
if (!model) return { state: currentState, exhausted: true, stateChanged: false };
|
|
717
|
+
let activated: boolean | void;
|
|
718
|
+
try {
|
|
719
|
+
activated = await port.activate(model);
|
|
720
|
+
} catch {
|
|
721
|
+
return { state: currentState, exhausted: false, stateChanged: false };
|
|
722
|
+
}
|
|
723
|
+
if (activated === false) return { state: currentState, exhausted: false, stateChanged: false };
|
|
724
|
+
const nextState: ModelRouteState = {
|
|
725
|
+
...currentState,
|
|
726
|
+
version: 2,
|
|
727
|
+
activeFallback: level,
|
|
728
|
+
lastFailover: {
|
|
729
|
+
stage: currentState.activeStage,
|
|
730
|
+
failedSelector: pausedSelector,
|
|
731
|
+
fallbackLevel: level,
|
|
732
|
+
activatedSelector: selection.selector,
|
|
733
|
+
switchedAt: port.now?.() ?? new Date().toISOString(),
|
|
734
|
+
},
|
|
735
|
+
};
|
|
736
|
+
const persistenceError = persistFailoverState(port, nextState);
|
|
737
|
+
return {
|
|
738
|
+
state: nextState,
|
|
739
|
+
switched: { level, selection },
|
|
740
|
+
exhausted: false,
|
|
741
|
+
stateChanged: true,
|
|
742
|
+
...(persistenceError ? { persistenceError } : {}),
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
586
746
|
export async function executeAutomaticFailover(
|
|
587
747
|
currentState: ModelRouteState,
|
|
588
748
|
failedSelector: string,
|
|
@@ -780,7 +940,8 @@ export async function executeModelRoute(
|
|
|
780
940
|
};
|
|
781
941
|
}
|
|
782
942
|
}
|
|
783
|
-
const
|
|
943
|
+
const usage = providerUsageSnapshots(port.models);
|
|
944
|
+
const formatted = formatCatalogPresentation(port.runtimeName, catalog, requiredStages, providerFilter, usage);
|
|
784
945
|
return {
|
|
785
946
|
state: currentState,
|
|
786
947
|
response: toolResult(formatted.text, {
|
package/core/provider-config.ts
CHANGED
|
@@ -15,6 +15,28 @@ export interface ProviderFiles {
|
|
|
15
15
|
envPath: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const KIMI_CODE_MODEL_IDS = [
|
|
19
|
+
"kimi-for-coding",
|
|
20
|
+
"k3",
|
|
21
|
+
"k3-256k",
|
|
22
|
+
"kimi-for-coding-highspeed",
|
|
23
|
+
"kimi-k2",
|
|
24
|
+
"kimi-k2.5",
|
|
25
|
+
"kimi-k2-turbo-preview",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
/** Optional Kimi Code models.yml input. It names the env var and does not carry an API key. */
|
|
29
|
+
export function kimiCodeProviderPreset(): CustomProviderInput {
|
|
30
|
+
return {
|
|
31
|
+
providerId: "kimi-code",
|
|
32
|
+
displayName: "Kimi Code",
|
|
33
|
+
baseUrl: "https://api.kimi.com/coding/v1",
|
|
34
|
+
envVar: "KIMI_API_KEY",
|
|
35
|
+
modelIds: [...KIMI_CODE_MODEL_IDS],
|
|
36
|
+
allowLocalHttp: false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
18
40
|
const PROVIDER_ID = /^[a-z0-9_-]+$/;
|
|
19
41
|
const ENV_VAR = /^[A-Z][A-Z0-9_]*$/;
|
|
20
42
|
const MODEL_ID = /^[A-Za-z0-9._:-]+$/;
|
package/package.json
CHANGED
package/scripts/install.sh
CHANGED
|
@@ -71,4 +71,4 @@ if [[ "${runtime}" == "omp" || "${runtime}" == "both" ]]; then
|
|
|
71
71
|
omp plugin install --scope user pi-marg@pi-marg-marketplace
|
|
72
72
|
fi
|
|
73
73
|
|
|
74
|
-
echo "PiMarg v1.0.
|
|
74
|
+
echo "PiMarg v1.0.8 installed for ${runtime}. Start a fresh runtime session, then run /pi-marg."
|
package/scripts/validate.sh
CHANGED
|
@@ -45,7 +45,7 @@ bun -e '
|
|
|
45
45
|
const packageJson = await Bun.file("package.json").json();
|
|
46
46
|
const marketplace = await Bun.file(".omp-plugin/marketplace.json").json();
|
|
47
47
|
if (packageJson.name !== "@sangyaahub/pi-marg") throw new Error("Unexpected package name");
|
|
48
|
-
if (packageJson.version !== "1.0.
|
|
48
|
+
if (packageJson.version !== "1.0.8") throw new Error("package.json must be v1.0.8");
|
|
49
49
|
if (packageJson.license !== "MIT") throw new Error("package.json must declare MIT");
|
|
50
50
|
if (packageJson.author !== "Sangyaa") throw new Error("Unexpected package author");
|
|
51
51
|
if (!packageJson.keywords?.includes("pi-package")) throw new Error("Pi catalog keyword is missing");
|