@north-light/crouter 0.3.161 → 0.3.163
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/api/client.d.ts +3 -1
- package/dist/api/client.js +6 -0
- package/dist/api/dto/modelauth.d.ts +32 -0
- package/dist/api/dto/modelauth.js +2 -1
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/00-runtime-base.md +1 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -0
- package/dist/builtin-memory/init.md +38 -0
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +0 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +88 -396
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +215 -419
- package/dist/clients/attach/__tests__/crtr-output.test.js +14 -9
- package/dist/clients/attach/__tests__/edit-diff.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/edit-diff.test.js +34 -0
- package/dist/clients/attach/__tests__/editor-frame-title.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/editor-frame-title.test.js +19 -0
- package/dist/clients/attach/render/chat-view.js +8 -3
- package/dist/clients/attach/render/crtr-output.js +31 -2
- package/dist/clients/attach/render/edit-diff.js +26 -2
- package/dist/clients/attach/render/tool-calls.d.ts +7 -2
- package/dist/clients/attach/render/tool-calls.js +25 -4
- package/dist/clients/attach/session/connection.d.ts +1 -1
- package/dist/clients/attach/session/editor-frame.d.ts +5 -1
- package/dist/clients/attach/session/editor-frame.js +21 -15
- package/dist/clients/attach/session/identity.d.ts +1 -1
- package/dist/clients/attach/viewer.js +441 -439
- package/dist/commands/__tests__/api-canvas-source.test.d.ts +1 -0
- package/dist/commands/__tests__/api-canvas-source.test.js +37 -0
- package/dist/commands/__tests__/human.test.js +2 -0
- package/dist/commands/__tests__/search-contents.test.d.ts +1 -0
- package/dist/commands/__tests__/search-contents.test.js +52 -0
- package/dist/commands/api-client.js +1 -0
- package/dist/commands/human/prompts.js +2 -2
- package/dist/commands/human/shared.d.ts +1 -0
- package/dist/commands/human/shared.js +7 -4
- package/dist/commands/memory/lint.d.ts +6 -0
- package/dist/commands/memory/lint.js +177 -35
- package/dist/commands/memory/read.js +22 -1
- package/dist/commands/memory/write.js +18 -4
- package/dist/commands/memory.js +1 -1
- package/dist/commands/search/contents.js +2 -2
- package/dist/commands/sys/__tests__/sync-deps.test.js +12 -21
- package/dist/commands/sys/__tests__/sync-import.test.js +30 -29
- package/dist/commands/sys/setup-wizard.d.ts +8 -1
- package/dist/commands/sys/setup-wizard.js +209 -58
- package/dist/commands/sys/sync-deps.js +14 -18
- package/dist/commands/sys/sync-project-guidance.js +16 -19
- package/dist/core/__tests__/broker-launch-candidates.test.js +13 -3
- package/dist/core/__tests__/broker-sdk-wiring.test.js +53 -25
- package/dist/core/__tests__/full/broker-provider-retry.test.js +6 -122
- package/dist/core/__tests__/migration.test.js +8 -3
- package/dist/core/__tests__/model-pin-durability.test.js +7 -5
- package/dist/core/__tests__/model-routes.test.d.ts +1 -0
- package/dist/core/__tests__/model-routes.test.js +41 -0
- package/dist/core/__tests__/on-read-crouter-home-fence.test.js +7 -10
- package/dist/core/__tests__/on-read-dedup-resume.test.js +13 -25
- package/dist/core/__tests__/on-read-identity.test.js +8 -15
- package/dist/core/__tests__/review-model-floor.test.js +2 -2
- package/dist/core/__tests__/revive.test.js +2 -2
- package/dist/core/__tests__/tmux-surface.test.js +10 -4
- package/dist/core/__tests__/worktree.test.js +3 -3
- package/dist/core/auth-file.d.ts +3 -2
- package/dist/core/auth-file.js +5 -3
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +18 -11
- package/dist/core/canvas/labels.d.ts +4 -5
- package/dist/core/canvas/labels.js +9 -9
- package/dist/core/canvas/types.d.ts +6 -1
- package/dist/core/command-manifests/schema.js +18 -5
- package/dist/core/command.js +17 -7
- package/dist/core/config.d.ts +3 -1
- package/dist/core/config.js +88 -2
- package/dist/core/configured-clis/invoker.js +2 -0
- package/dist/core/help.d.ts +3 -1
- package/dist/core/help.js +5 -2
- package/dist/core/keybindings/__tests__/resolve.test.js +5 -2
- package/dist/core/keybindings/catalog.d.ts +3 -2
- package/dist/core/keybindings/catalog.js +34 -25
- package/dist/core/keybindings/index.d.ts +1 -1
- package/dist/core/keybindings/resolve.js +8 -0
- package/dist/core/keybindings/types.d.ts +6 -1
- package/dist/core/memory/doc-link-grammar.d.ts +20 -0
- package/dist/core/memory/doc-link-grammar.js +110 -0
- package/dist/core/memory-resolver.d.ts +5 -0
- package/dist/core/memory-resolver.js +12 -1
- package/dist/core/model-routes.d.ts +37 -0
- package/dist/core/model-routes.js +140 -0
- package/dist/core/runtime/bearings.d.ts +8 -8
- package/dist/core/runtime/bearings.js +20 -16
- package/dist/core/runtime/broker.d.ts +0 -4
- package/dist/core/runtime/broker.js +119 -385
- package/dist/core/runtime/canvas-extensions.d.ts +1 -0
- package/dist/core/runtime/canvas-extensions.js +2 -0
- package/dist/core/runtime/launch.d.ts +10 -12
- package/dist/core/runtime/launch.js +16 -22
- package/dist/core/runtime/managed-provider-cooling.d.ts +0 -14
- package/dist/core/runtime/managed-provider-cooling.js +0 -19
- package/dist/core/runtime/model-swap.js +7 -1
- package/dist/core/runtime/promote.js +1 -0
- package/dist/core/runtime/tmux.js +136 -100
- package/dist/core/scope.js +4 -5
- package/dist/core/subscription-state.d.ts +12 -36
- package/dist/core/subscription-state.js +19 -203
- package/dist/core/substrate/ceiling.d.ts +3 -6
- package/dist/core/substrate/ceiling.js +13 -15
- package/dist/core/substrate/index.d.ts +2 -3
- package/dist/core/substrate/index.js +2 -2
- package/dist/core/substrate/on-read.d.ts +4 -13
- package/dist/core/substrate/on-read.js +142 -262
- package/dist/core/substrate/render.js +4 -4
- package/dist/core/substrate/schema.d.ts +3 -2
- package/dist/core/substrate/schema.js +5 -5
- package/dist/daemon/api/__tests__/full/api-server.test.js +9 -6
- package/dist/daemon/api/__tests__/full/b10-attach-modelauth.test.js +123 -1
- package/dist/daemon/api/handlers/modelauth.js +51 -2
- package/dist/daemon/api/handlers/nodes.js +2 -0
- package/dist/pi-extensions/__tests__/canvas-tool-guide.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-tool-guide.test.js +96 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +7 -8
- package/dist/pi-extensions/canvas-tool-guide.d.ts +14 -0
- package/dist/pi-extensions/canvas-tool-guide.js +74 -0
- package/dist/types.d.ts +17 -0
- package/dist/web-client/assets/{index-B76ZKfT_.js → index-NIuSCOHM.js} +1 -1
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crouter-help.ts +0 -95
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import type { ThinkingLevel } from "@earendil-works/pi-ai";
|
|
2
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
1
|
export declare const ANTHROPIC_PROVIDER_ID = "anthropic";
|
|
4
2
|
export declare const OPENAI_CODEX_PROVIDER_ID = "openai-codex";
|
|
5
3
|
export declare const INVALID_REFRESH_TOKEN_BACKOFF_MS: number;
|
|
6
4
|
export type ManagedProviderId = typeof ANTHROPIC_PROVIDER_ID | typeof OPENAI_CODEX_PROVIDER_ID;
|
|
7
|
-
export type LadderProviderId = "anthropic" | "openai";
|
|
8
5
|
export type ModelStrength = "ultra" | "strong" | "medium" | "light";
|
|
6
|
+
export type RotationConfig = {
|
|
7
|
+
defaultFallbackStrength?: ModelStrength;
|
|
8
|
+
revertWhenAvailable?: boolean;
|
|
9
|
+
};
|
|
10
|
+
type RotationConfigUpdate = RotationConfig;
|
|
9
11
|
export type SubscriptionCredential = {
|
|
10
12
|
label: string;
|
|
11
13
|
refresh: string;
|
|
@@ -17,27 +19,6 @@ export type SubscriptionCredential = {
|
|
|
17
19
|
authFailure?: "invalid_grant";
|
|
18
20
|
accountId?: string;
|
|
19
21
|
};
|
|
20
|
-
export type ModelRef = {
|
|
21
|
-
providerId: ManagedProviderId;
|
|
22
|
-
modelId: string;
|
|
23
|
-
};
|
|
24
|
-
export type RotationConfig = {
|
|
25
|
-
defaultFallbackStrength?: ModelStrength;
|
|
26
|
-
revertWhenAvailable?: boolean;
|
|
27
|
-
preferredModel?: ModelRef;
|
|
28
|
-
};
|
|
29
|
-
type RotationConfigUpdate = {
|
|
30
|
-
defaultFallbackStrength?: ModelStrength;
|
|
31
|
-
revertWhenAvailable?: boolean;
|
|
32
|
-
preferredModel?: ModelRef | null;
|
|
33
|
-
};
|
|
34
|
-
export type FallbackTarget = {
|
|
35
|
-
providerId: ManagedProviderId;
|
|
36
|
-
modelId: string;
|
|
37
|
-
label: string;
|
|
38
|
-
strength: ModelStrength;
|
|
39
|
-
thinkingLevel?: ThinkingLevel;
|
|
40
|
-
};
|
|
41
22
|
/** A concrete provider/model considered for either broker launch or turn-time rotation. */
|
|
42
23
|
export type ProviderCandidate = {
|
|
43
24
|
providerId: string;
|
|
@@ -49,6 +30,12 @@ export type ProviderCandidate = {
|
|
|
49
30
|
coolingUntil?: number;
|
|
50
31
|
/** False when this candidate may only be selected by an explicit request, never as fallback. */
|
|
51
32
|
automaticFallback?: boolean;
|
|
33
|
+
/** Route provenance carried through selection diagnostics. */
|
|
34
|
+
routeId?: string;
|
|
35
|
+
credentialSource?: string;
|
|
36
|
+
family?: string;
|
|
37
|
+
strength?: ModelStrength;
|
|
38
|
+
rank?: number;
|
|
52
39
|
};
|
|
53
40
|
export type ProviderCandidateRejection = "unregistered" | "non-managed-fallback" | "unauthenticated" | "cooling";
|
|
54
41
|
/**
|
|
@@ -72,12 +59,6 @@ export declare function resolveProviderCandidates(candidates: readonly ProviderC
|
|
|
72
59
|
}>;
|
|
73
60
|
};
|
|
74
61
|
export declare function isManagedProvider(providerId: string | undefined): providerId is ManagedProviderId;
|
|
75
|
-
/** The `modelLadders` CONFIG key for a runtime provider id -- `openai-codex`
|
|
76
|
-
* (the runtime/auth id) maps to the `openai` ladder key used in
|
|
77
|
-
* ~/.crouter/config.json. Callers building a repair command that a user can
|
|
78
|
-
* paste into `crtr sys config set modelLadders.<key>...` must use THIS key,
|
|
79
|
-
* not the runtime provider id. */
|
|
80
|
-
export declare function getLadderProviderId(providerId: ManagedProviderId): LadderProviderId;
|
|
81
62
|
export declare function getProviderLabel(providerId: ManagedProviderId): string;
|
|
82
63
|
export declare function mutateSubscriptionPool(providerId: ManagedProviderId, fn: (pool: SubscriptionCredential[], assertLockHeld: () => void) => SubscriptionCredential[] | undefined | Promise<SubscriptionCredential[] | undefined>): Promise<SubscriptionCredential[]>;
|
|
83
64
|
export declare function refreshSubscriptionCredential(providerId: ManagedProviderId, label: string, exchange: (refreshToken: string) => Promise<{
|
|
@@ -118,6 +99,7 @@ export declare function commitManagedLogin(providerId: ManagedProviderId, label:
|
|
|
118
99
|
lastRateLimitedAt: number;
|
|
119
100
|
autoLabel?: boolean;
|
|
120
101
|
}): Promise<SubscriptionCredential[]>;
|
|
102
|
+
export declare function removeAllManagedAccounts(providerId: ManagedProviderId): Promise<number>;
|
|
121
103
|
export declare function removeManagedProvider(providerId: ManagedProviderId, label?: string): Promise<SubscriptionCredential[]>;
|
|
122
104
|
export declare function removeSubscription(providerId: ManagedProviderId, ref: string): Promise<SubscriptionCredential[]>;
|
|
123
105
|
export declare function promoteSubscription(providerId: ManagedProviderId, ref: string): Promise<SubscriptionCredential[]>;
|
|
@@ -128,11 +110,5 @@ export declare function markSubscriptionSuccess(providerId: ManagedProviderId, r
|
|
|
128
110
|
export declare function parseRetryAfterHeader(value: string | undefined, now?: number): number | undefined;
|
|
129
111
|
export declare function readRotationConfig(): RotationConfig;
|
|
130
112
|
export declare function writeRotationConfig(next: RotationConfigUpdate): RotationConfig;
|
|
131
|
-
export declare function clearPreferredModel(): RotationConfig;
|
|
132
|
-
export declare function rememberPreferredModel(model: ModelRef): RotationConfig;
|
|
133
|
-
export declare function resolveFallbackTarget(currentProviderId: ManagedProviderId, currentModelId?: string, config?: RotationConfig, currentThinkingLevel?: string): FallbackTarget | undefined;
|
|
134
|
-
export declare function shouldRestorePreferredModel(config?: RotationConfig, now?: number): boolean;
|
|
135
|
-
export declare function restorePreferredModelIfPossible(piApi: ExtensionAPI, ctx: Pick<ExtensionContext, "model" | "modelRegistry">): Promise<boolean>;
|
|
136
|
-
export declare function switchToFallbackIfPossible(piApi: ExtensionAPI, ctx: Pick<ExtensionContext, "model" | "modelRegistry">, preferredModel?: ModelRef | undefined): Promise<FallbackTarget | undefined>;
|
|
137
113
|
export declare function formatStatusLine(providerId: ManagedProviderId, now?: number): string;
|
|
138
114
|
export {};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { defaultModelLaddersConfig } from "../types.js";
|
|
4
3
|
import { homedir } from "node:os";
|
|
5
4
|
import { dirname, join } from "node:path";
|
|
6
5
|
import { LockedJsonFile } from "./locked-file.js";
|
|
@@ -61,19 +60,14 @@ function agentDir() {
|
|
|
61
60
|
function rotationConfigFile() {
|
|
62
61
|
return join(agentDir(), "provider-rotation.json");
|
|
63
62
|
}
|
|
64
|
-
function crouterConfigFile() {
|
|
65
|
-
return join(homedir(), ".crouter", "config.json");
|
|
66
|
-
}
|
|
67
63
|
const DEFAULT_FALLBACK_STRENGTH = "strong";
|
|
68
64
|
const PROVIDERS = {
|
|
69
65
|
[ANTHROPIC_PROVIDER_ID]: {
|
|
70
66
|
label: "Claude",
|
|
71
|
-
ladderProviderId: "anthropic",
|
|
72
67
|
poolFileName: "anthropic-subscriptions.json",
|
|
73
68
|
},
|
|
74
69
|
[OPENAI_CODEX_PROVIDER_ID]: {
|
|
75
70
|
label: "OpenAI Codex",
|
|
76
|
-
ladderProviderId: "openai",
|
|
77
71
|
poolFileName: "openai-codex-subscriptions.json",
|
|
78
72
|
},
|
|
79
73
|
};
|
|
@@ -184,9 +178,6 @@ function traceDroppedRows(providerId, stored, normalized) {
|
|
|
184
178
|
return;
|
|
185
179
|
process.stderr.write(`[subscription-pool] ${getProviderLabel(providerId)}: dropping ${stored.length - normalized.length} stored row(s) with no label or no token pair\n`);
|
|
186
180
|
}
|
|
187
|
-
function getCurrentModelRef(model) {
|
|
188
|
-
return isManagedProvider(model?.provider) ? { providerId: model.provider, modelId: model.id } : undefined;
|
|
189
|
-
}
|
|
190
181
|
// A refreshed/modified token set is only storable when it carries BOTH halves and a usable
|
|
191
182
|
// expiry. Persisting a partial one is silently destructive: `JSON.stringify` drops the missing
|
|
192
183
|
// key, and `normalizeCredential` (which requires refresh AND access together) then discards the
|
|
@@ -247,14 +238,6 @@ function logCredentialSetChange(providerId, base, next) {
|
|
|
247
238
|
export function isManagedProvider(providerId) {
|
|
248
239
|
return providerId === ANTHROPIC_PROVIDER_ID || providerId === OPENAI_CODEX_PROVIDER_ID;
|
|
249
240
|
}
|
|
250
|
-
/** The `modelLadders` CONFIG key for a runtime provider id -- `openai-codex`
|
|
251
|
-
* (the runtime/auth id) maps to the `openai` ladder key used in
|
|
252
|
-
* ~/.crouter/config.json. Callers building a repair command that a user can
|
|
253
|
-
* paste into `crtr sys config set modelLadders.<key>...` must use THIS key,
|
|
254
|
-
* not the runtime provider id. */
|
|
255
|
-
export function getLadderProviderId(providerId) {
|
|
256
|
-
return PROVIDERS[providerId].ladderProviderId;
|
|
257
|
-
}
|
|
258
241
|
export function getProviderLabel(providerId) {
|
|
259
242
|
return PROVIDERS[providerId].label;
|
|
260
243
|
}
|
|
@@ -603,14 +586,26 @@ export async function commitManagedLogin(providerId, label, credential) {
|
|
|
603
586
|
return merged;
|
|
604
587
|
});
|
|
605
588
|
}
|
|
589
|
+
// Remove every account for one managed provider through a single locked transform. The
|
|
590
|
+
// returned count belongs to that locked snapshot, so callers do not race a concurrent login.
|
|
591
|
+
export async function removeAllManagedAccounts(providerId) {
|
|
592
|
+
let removed = 0;
|
|
593
|
+
await mutateSubscriptionPool(providerId, (pool) => {
|
|
594
|
+
removed = pool.length;
|
|
595
|
+
return [];
|
|
596
|
+
});
|
|
597
|
+
return removed;
|
|
598
|
+
}
|
|
606
599
|
// Coordinated removal for the human UI. Removes a single account when `label` is given; the ENTIRE
|
|
607
|
-
// provider (every pool account) when `label` is omitted — always through
|
|
600
|
+
// provider (every pool account) when `label` is omitted — always through a pool transform. UI
|
|
608
601
|
// must NEVER reach for a raw auth.json delete on a managed provider: that strips a file pi no
|
|
609
602
|
// longer reads for managed providers and orphans the whole pool.
|
|
610
603
|
export async function removeManagedProvider(providerId, label) {
|
|
604
|
+
if (label === undefined) {
|
|
605
|
+
await removeAllManagedAccounts(providerId);
|
|
606
|
+
return [];
|
|
607
|
+
}
|
|
611
608
|
return mutateSubscriptionPool(providerId, (pool) => {
|
|
612
|
-
if (label === undefined)
|
|
613
|
-
return [];
|
|
614
609
|
const index = resolveCredentialIndex(pool, label);
|
|
615
610
|
if (index === -1)
|
|
616
611
|
throw new Error(`Unknown ${getProviderLabel(providerId)} subscription: ${label}`);
|
|
@@ -701,9 +696,6 @@ export async function markSubscriptionSuccess(providerId, ref, attemptAt = Date.
|
|
|
701
696
|
return next;
|
|
702
697
|
});
|
|
703
698
|
}
|
|
704
|
-
function findAvailableSubscription(providerId, now = Date.now()) {
|
|
705
|
-
return readSubscriptionPool(providerId).find((entry) => !entry.rateLimitedUntil || entry.rateLimitedUntil <= now);
|
|
706
|
-
}
|
|
707
699
|
export function parseRetryAfterHeader(value, now = Date.now()) {
|
|
708
700
|
if (!value)
|
|
709
701
|
return undefined;
|
|
@@ -716,102 +708,11 @@ export function parseRetryAfterHeader(value, now = Date.now()) {
|
|
|
716
708
|
const parsed = Date.parse(trimmed);
|
|
717
709
|
return Number.isFinite(parsed) ? Math.max(0, parsed - now) : undefined;
|
|
718
710
|
}
|
|
719
|
-
function thinkingLevelOfRef(ref) {
|
|
720
|
-
const match = ref.trim().match(/:(thinking|off|minimal|low|medium|high|xhigh|max)$/i);
|
|
721
|
-
if (!match)
|
|
722
|
-
return undefined;
|
|
723
|
-
const level = match[1].toLowerCase();
|
|
724
|
-
return level === "off" || level === "thinking" ? undefined : level;
|
|
725
|
-
}
|
|
726
|
-
function normalizeModelRef(ref) {
|
|
727
|
-
return ref
|
|
728
|
-
.trim()
|
|
729
|
-
.replace(/^[^/]+\//, "")
|
|
730
|
-
.replace(/:(?:thinking|off|minimal|low|medium|high|xhigh|max)$/i, "")
|
|
731
|
-
.toLowerCase();
|
|
732
|
-
}
|
|
733
|
-
function splitModelRef(ref) {
|
|
734
|
-
const trimmed = ref.trim();
|
|
735
|
-
const slash = trimmed.indexOf("/");
|
|
736
|
-
const providerId = slash === -1 ? OPENAI_CODEX_PROVIDER_ID : trimmed.slice(0, slash).trim();
|
|
737
|
-
if (!isManagedProvider(providerId)) {
|
|
738
|
-
throw new Error(`Unsupported fallback provider: ${providerId}`);
|
|
739
|
-
}
|
|
740
|
-
const rawModelId = slash === -1 ? trimmed : trimmed.slice(slash + 1).trim();
|
|
741
|
-
const thinkingMatch = rawModelId.match(/^(.*?)(?::(thinking|off|minimal|low|medium|high|xhigh|max))$/i);
|
|
742
|
-
return {
|
|
743
|
-
providerId,
|
|
744
|
-
modelId: thinkingMatch ? thinkingMatch[1] : rawModelId,
|
|
745
|
-
label: `${providerId}/${thinkingMatch ? thinkingMatch[1] : rawModelId}${thinkingMatch?.[2] ? `:${thinkingMatch[2].toLowerCase()}` : ""}`,
|
|
746
|
-
strength: DEFAULT_FALLBACK_STRENGTH,
|
|
747
|
-
thinkingLevel: thinkingMatch?.[2] && thinkingMatch[2].toLowerCase() !== "off" && thinkingMatch[2].toLowerCase() !== "thinking"
|
|
748
|
-
? thinkingMatch[2].toLowerCase()
|
|
749
|
-
: undefined,
|
|
750
|
-
};
|
|
751
|
-
}
|
|
752
|
-
// Layers ~/.crouter/config.json's `modelLadders` (if any) OVER crouter's
|
|
753
|
-
// builtin ladder (`defaultModelLaddersConfig()`, src/types.ts — imported
|
|
754
|
-
// directly: this package's canonical home is the crouter tree, so the
|
|
755
|
-
// relative import always resolves, to dist/types.js at runtime), per
|
|
756
|
-
// provider/strength — mirroring crouter's own
|
|
757
|
-
// `mergeModelLadders` (src/core/config.ts), which is what a node's launcher
|
|
758
|
-
// actually resolves models from. Without this fallback, a config-less user
|
|
759
|
-
// (the common case: fresh install, no ~/.crouter/config.json) saw an EMPTY
|
|
760
|
-
// ladder here while crtr itself launched them on the builtin defaults —
|
|
761
|
-
// rotation could then never resolve a strength tier or a cross-provider
|
|
762
|
-
// fallback for a model it was already running. Always returns a fully
|
|
763
|
-
// populated ladder for both providers; a config.json override only replaces
|
|
764
|
-
// the specific strengths it sets.
|
|
765
|
-
function readModelLadders() {
|
|
766
|
-
const config = readJsonFile(crouterConfigFile());
|
|
767
|
-
const overrides = config?.modelLadders;
|
|
768
|
-
const builtin = defaultModelLaddersConfig();
|
|
769
|
-
const result = {};
|
|
770
|
-
for (const provider of ["anthropic", "openai"]) {
|
|
771
|
-
const normalized = { ...builtin[provider] };
|
|
772
|
-
const override = overrides?.[provider];
|
|
773
|
-
if (override) {
|
|
774
|
-
for (const strength of ["ultra", "strong", "medium", "light"]) {
|
|
775
|
-
const value = typeof override[strength] === "string" ? override[strength]?.trim() : "";
|
|
776
|
-
if (value)
|
|
777
|
-
normalized[strength] = value;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
result[provider] = normalized;
|
|
781
|
-
}
|
|
782
|
-
return result;
|
|
783
|
-
}
|
|
784
|
-
function findStrengthForModelId(providerId, modelId, defaultStrength, ladders = readModelLadders(), currentThinkingLevel) {
|
|
785
|
-
const source = ladders?.[PROVIDERS[providerId].ladderProviderId];
|
|
786
|
-
const bareId = modelId ? normalizeModelRef(modelId) : undefined;
|
|
787
|
-
if (!source || !bareId)
|
|
788
|
-
return undefined;
|
|
789
|
-
const matches = [];
|
|
790
|
-
for (const strength of ["ultra", "strong", "medium", "light"]) {
|
|
791
|
-
const ref = source[strength];
|
|
792
|
-
if (ref && normalizeModelRef(ref) === bareId)
|
|
793
|
-
matches.push(strength);
|
|
794
|
-
}
|
|
795
|
-
if (matches.length === 0)
|
|
796
|
-
return undefined;
|
|
797
|
-
// Rungs that share a bare model id (e.g. gpt-5.6-sol:max vs gpt-5.6-sol:high) are
|
|
798
|
-
// disambiguated by the live reasoning level so an ultra run doesn't collapse to strong.
|
|
799
|
-
if (matches.length > 1 && currentThinkingLevel) {
|
|
800
|
-
const exact = matches.find((strength) => thinkingLevelOfRef(source[strength]) === currentThinkingLevel);
|
|
801
|
-
if (exact)
|
|
802
|
-
return exact;
|
|
803
|
-
}
|
|
804
|
-
return matches.includes(defaultStrength) ? defaultStrength : matches[0];
|
|
805
|
-
}
|
|
806
711
|
export function readRotationConfig() {
|
|
807
712
|
const raw = readJsonFile(rotationConfigFile()) ?? {};
|
|
808
|
-
const preferredModel = raw.preferredModel && isManagedProvider(raw.preferredModel.providerId) && typeof raw.preferredModel.modelId === "string"
|
|
809
|
-
? { providerId: raw.preferredModel.providerId, modelId: raw.preferredModel.modelId.trim() }
|
|
810
|
-
: undefined;
|
|
811
713
|
return {
|
|
812
714
|
defaultFallbackStrength: normalizeStrength(raw.defaultFallbackStrength) ?? DEFAULT_FALLBACK_STRENGTH,
|
|
813
715
|
revertWhenAvailable: raw.revertWhenAvailable ?? true,
|
|
814
|
-
preferredModel,
|
|
815
716
|
};
|
|
816
717
|
}
|
|
817
718
|
export function writeRotationConfig(next) {
|
|
@@ -819,100 +720,15 @@ export function writeRotationConfig(next) {
|
|
|
819
720
|
const merged = {
|
|
820
721
|
defaultFallbackStrength: normalizeStrength(next.defaultFallbackStrength) ?? current.defaultFallbackStrength ?? DEFAULT_FALLBACK_STRENGTH,
|
|
821
722
|
revertWhenAvailable: next.revertWhenAvailable ?? current.revertWhenAvailable ?? true,
|
|
822
|
-
preferredModel: Object.prototype.hasOwnProperty.call(next, "preferredModel")
|
|
823
|
-
? next.preferredModel || undefined
|
|
824
|
-
: current.preferredModel,
|
|
825
723
|
};
|
|
826
724
|
writeJsonFile(rotationConfigFile(), merged);
|
|
827
725
|
return merged;
|
|
828
726
|
}
|
|
829
|
-
export function clearPreferredModel() {
|
|
830
|
-
return writeRotationConfig({ preferredModel: null });
|
|
831
|
-
}
|
|
832
|
-
export function rememberPreferredModel(model) {
|
|
833
|
-
return writeRotationConfig({ preferredModel: model });
|
|
834
|
-
}
|
|
835
|
-
// `readModelLadders()` always fills both providers from the builtin defaults, so this
|
|
836
|
-
// only returns undefined (never throws) in the defensive case where a rung is still
|
|
837
|
-
// genuinely missing; callers must treat that as a graceful "no fallback available" case
|
|
838
|
-
// rather than a config error to surface mid-turn.
|
|
839
|
-
export function resolveFallbackTarget(currentProviderId, currentModelId, config = readRotationConfig(), currentThinkingLevel) {
|
|
840
|
-
const ladders = readModelLadders();
|
|
841
|
-
const defaultStrength = config.defaultFallbackStrength ?? DEFAULT_FALLBACK_STRENGTH;
|
|
842
|
-
const sourceStrength = findStrengthForModelId(currentProviderId, currentModelId, defaultStrength, ladders, currentThinkingLevel);
|
|
843
|
-
const strength = sourceStrength ?? defaultStrength;
|
|
844
|
-
const targetProvider = currentProviderId === ANTHROPIC_PROVIDER_ID ? OPENAI_CODEX_PROVIDER_ID : ANTHROPIC_PROVIDER_ID;
|
|
845
|
-
const targetLadder = ladders?.[PROVIDERS[targetProvider].ladderProviderId];
|
|
846
|
-
const candidates = strength === defaultStrength ? [strength] : [strength, defaultStrength];
|
|
847
|
-
const fallbackRef = candidates.map((candidate) => targetLadder?.[candidate]).find((ref) => typeof ref === "string" && ref.trim().length > 0);
|
|
848
|
-
if (!fallbackRef) {
|
|
849
|
-
return undefined;
|
|
850
|
-
}
|
|
851
|
-
const parsed = splitModelRef(fallbackRef);
|
|
852
|
-
const resolvedStrength = candidates.find((candidate) => targetLadder?.[candidate] === fallbackRef) ?? strength;
|
|
853
|
-
return { ...parsed, strength: resolvedStrength };
|
|
854
|
-
}
|
|
855
|
-
export function shouldRestorePreferredModel(config = readRotationConfig(), now = Date.now()) {
|
|
856
|
-
if (!config.revertWhenAvailable || !config.preferredModel)
|
|
857
|
-
return false;
|
|
858
|
-
return Boolean(findAvailableSubscription(config.preferredModel.providerId, now));
|
|
859
|
-
}
|
|
860
|
-
export async function restorePreferredModelIfPossible(piApi, ctx) {
|
|
861
|
-
const config = readRotationConfig();
|
|
862
|
-
if (!shouldRestorePreferredModel(config, Date.now()))
|
|
863
|
-
return false;
|
|
864
|
-
const preferred = config.preferredModel;
|
|
865
|
-
if (!preferred)
|
|
866
|
-
return false;
|
|
867
|
-
const current = getCurrentModelRef(ctx.model);
|
|
868
|
-
if (current?.providerId === preferred.providerId && current.modelId === preferred.modelId) {
|
|
869
|
-
clearPreferredModel();
|
|
870
|
-
return true;
|
|
871
|
-
}
|
|
872
|
-
const model = ctx.modelRegistry.find(preferred.providerId, preferred.modelId);
|
|
873
|
-
if (!model)
|
|
874
|
-
return false;
|
|
875
|
-
const switched = await piApi.setModel(model);
|
|
876
|
-
if (switched)
|
|
877
|
-
clearPreferredModel();
|
|
878
|
-
return switched;
|
|
879
|
-
}
|
|
880
|
-
export async function switchToFallbackIfPossible(piApi, ctx, preferredModel = getCurrentModelRef(ctx.model)) {
|
|
881
|
-
if (!preferredModel)
|
|
882
|
-
return undefined;
|
|
883
|
-
rememberPreferredModel(preferredModel);
|
|
884
|
-
const currentThinkingLevel = piApi.getThinkingLevel?.();
|
|
885
|
-
const target = resolveFallbackTarget(preferredModel.providerId, preferredModel.modelId, readRotationConfig(), currentThinkingLevel);
|
|
886
|
-
if (!target)
|
|
887
|
-
return undefined;
|
|
888
|
-
const current = getCurrentModelRef(ctx.model);
|
|
889
|
-
const model = ctx.modelRegistry.find(target.providerId, target.modelId);
|
|
890
|
-
if (!model)
|
|
891
|
-
return undefined;
|
|
892
|
-
if (current?.providerId !== target.providerId || current.modelId !== target.modelId) {
|
|
893
|
-
const switched = await piApi.setModel(model);
|
|
894
|
-
if (!switched) {
|
|
895
|
-
// `setModel` returns false when its target lacks configured auth and leaves
|
|
896
|
-
// the live model unchanged. A concurrent transition can still independently
|
|
897
|
-
// make THIS exact target current while this call is in flight, so re-read
|
|
898
|
-
// the live model and accept only when it now equals the target on both provider and
|
|
899
|
-
// model id; any other current model is a genuine failed switch.
|
|
900
|
-
const afterSwitch = getCurrentModelRef(ctx.model);
|
|
901
|
-
if (afterSwitch?.providerId !== target.providerId || afterSwitch.modelId !== target.modelId)
|
|
902
|
-
return undefined;
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
if (target.thinkingLevel)
|
|
906
|
-
piApi.setThinkingLevel(target.thinkingLevel);
|
|
907
|
-
return target;
|
|
908
|
-
}
|
|
909
727
|
export function formatStatusLine(providerId, now = Date.now()) {
|
|
910
728
|
const pool = readSubscriptionPool(providerId);
|
|
911
|
-
const ready = pool.filter((entry) => !entry.rateLimitedUntil || entry.rateLimitedUntil <= now);
|
|
729
|
+
const ready = pool.filter((entry) => entry.authFailure === undefined && (!entry.rateLimitedUntil || entry.rateLimitedUntil <= now));
|
|
912
730
|
const label = getProviderLabel(providerId);
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
}
|
|
917
|
-
return `${label} ${ready[0]?.label ?? providerId} (${ready.length}/${pool.length} ready)`;
|
|
731
|
+
return ready.length === 0
|
|
732
|
+
? `${label} pool exhausted`
|
|
733
|
+
: `${label} ${ready[0]?.label ?? providerId} (${ready.length}/${pool.length} ready)`;
|
|
918
734
|
}
|
|
@@ -8,13 +8,10 @@ export declare function isIndexName(name: string): boolean;
|
|
|
8
8
|
export declare function indexDirOf(name: string): string;
|
|
9
9
|
/** Returns the display path corresponding to a canonical logical identifier. */
|
|
10
10
|
export declare function displayName(name: string): string;
|
|
11
|
-
/** The two visibility surfaces an INDEX can cap. */
|
|
12
|
-
export type Surface = 'systemPromptVisibility' | 'fileReadVisibility';
|
|
13
11
|
/** Build the dir → governing-INDEX map. Precedence is first-occurrence-wins, so
|
|
14
12
|
* callers MUST pass docs precedence-ordered (project > user > builtin) — the
|
|
15
13
|
* highest-precedence INDEX governs a dir, mirroring the memory resolver. */
|
|
16
14
|
export declare function buildCeilingIndex(docs: SubstrateDoc[]): Map<string, SubstrateDoc>;
|
|
17
|
-
/** A doc's effective rung
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
export declare function effectiveRung(doc: SubstrateDoc, ceil: Map<string, SubstrateDoc>, surface: Surface): Rung;
|
|
15
|
+
/** A doc's effective system-prompt rung after applying every ancestor INDEX
|
|
16
|
+
* ceiling. A `none` ancestor hides the whole subtree from the boot catalog. */
|
|
17
|
+
export declare function effectiveSystemPromptRung(doc: SubstrateDoc, ceil: Map<string, SubstrateDoc>): Rung;
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
// ceiling.ts — canonical INDEX identifiers and the subtree
|
|
1
|
+
// ceiling.ts — canonical INDEX identifiers and the boot-catalog subtree ceiling.
|
|
2
2
|
//
|
|
3
|
-
// A directory
|
|
4
|
-
// (`INDEX` at the root)
|
|
5
|
-
//
|
|
3
|
+
// A directory INDEX has the canonical logical identifier `<directory>/INDEX`
|
|
4
|
+
// (`INDEX` at the root). Its system-prompt rung caps every document in that
|
|
5
|
+
// subtree while the boot catalog renders:
|
|
6
6
|
//
|
|
7
7
|
// effective rung of a descendant = min(own rung, every ancestor INDEX's rung)
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// taste: `crtr memory read taste/document-substrate`.
|
|
9
|
+
// A `preview` INDEX therefore lets its descendants surface at most at preview;
|
|
10
|
+
// a `none` INDEX hides the subtree. File context does not use this ceiling:
|
|
11
|
+
// every workspace/file match is an explicit `applies-to` route and renders at
|
|
12
|
+
// the matched document's own file-read rung.
|
|
14
13
|
import { rungRank } from './schema.js';
|
|
15
14
|
/** The reserved basename (no extension) that marks a directory's index doc. */
|
|
16
15
|
export const INDEX_NAME = 'INDEX';
|
|
@@ -55,15 +54,14 @@ function ancestorDirs(name) {
|
|
|
55
54
|
out.push(parts.slice(0, i).join('/'));
|
|
56
55
|
return out;
|
|
57
56
|
}
|
|
58
|
-
/** A doc's effective rung
|
|
59
|
-
*
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
let rung = doc[surface];
|
|
57
|
+
/** A doc's effective system-prompt rung after applying every ancestor INDEX
|
|
58
|
+
* ceiling. A `none` ancestor hides the whole subtree from the boot catalog. */
|
|
59
|
+
export function effectiveSystemPromptRung(doc, ceil) {
|
|
60
|
+
let rung = doc.systemPromptVisibility;
|
|
63
61
|
for (const dir of ancestorDirs(doc.name)) {
|
|
64
62
|
const idx = ceil.get(dir);
|
|
65
63
|
if (idx)
|
|
66
|
-
rung = minRung(rung, idx
|
|
64
|
+
rung = minRung(rung, idx.systemPromptVisibility);
|
|
67
65
|
}
|
|
68
66
|
return rung;
|
|
69
67
|
}
|
|
@@ -4,8 +4,7 @@ export { scopeForCwd, profileNameFor } from './subject-fields.js';
|
|
|
4
4
|
export type { NodeConfigSubject } from './subject-fields.js';
|
|
5
5
|
export { spineDepth, assembleNodeSubject } from './subject.js';
|
|
6
6
|
export { gatePasses } from './gate.js';
|
|
7
|
-
export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex,
|
|
8
|
-
export type { Surface } from './ceiling.js';
|
|
7
|
+
export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveSystemPromptRung, } from './ceiling.js';
|
|
9
8
|
export { renderPreferencesForSubject, renderKnowledgeForSubject } from './render.js';
|
|
10
9
|
export { renderPreferencesSection, renderKnowledgeBlock } from './render-node.js';
|
|
11
|
-
export { renderOnReadDocs } from './on-read.js';
|
|
10
|
+
export { renderOnReadDocs, renderWorkspaceOpenDocs } from './on-read.js';
|
|
@@ -19,7 +19,7 @@ resolveDocName, } from './schema.js';
|
|
|
19
19
|
export { scopeForCwd, profileNameFor } from './subject-fields.js';
|
|
20
20
|
export { spineDepth, assembleNodeSubject } from './subject.js';
|
|
21
21
|
export { gatePasses } from './gate.js';
|
|
22
|
-
export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex,
|
|
22
|
+
export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveSystemPromptRung, } from './ceiling.js';
|
|
23
23
|
export { renderPreferencesForSubject, renderKnowledgeForSubject } from './render.js';
|
|
24
24
|
export { renderPreferencesSection, renderKnowledgeBlock } from './render-node.js';
|
|
25
|
-
export { renderOnReadDocs } from './on-read.js';
|
|
25
|
+
export { renderOnReadDocs, renderWorkspaceOpenDocs } from './on-read.js';
|
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Render the substrate docs that should surface alongside a just-read file.
|
|
3
|
-
*
|
|
4
|
-
* @param nodeId the canvas node whose subject gates the docs.
|
|
5
|
-
* @param readFilePath the path the `read` tool returned (absolute or not — it
|
|
6
|
-
* is resolved to a realpath internally).
|
|
7
|
-
* @param seen the CALLER-owned, per-session set of already-injected
|
|
8
|
-
* doc realpaths. Docs already present are skipped; newly
|
|
9
|
-
* injected docs are added. Pass the same set across reads
|
|
10
|
-
* within a session (clear it on session_start) to get the
|
|
11
|
-
* once-per-session dedup; omit it for a standalone render.
|
|
12
|
-
* @returns the `<auto-loaded-context>` envelope, or '' when nothing surfaces.
|
|
13
|
-
*/
|
|
1
|
+
/** Surface docs matched by a successful read tool call. */
|
|
14
2
|
export declare function renderOnReadDocs(nodeId: string, readFilePath: string, seen?: Set<string>): string;
|
|
3
|
+
/** Surface workspace-wide docs during first-message assembly. `.` is a reserved
|
|
4
|
+
* applies-to target meaning “when this project store is mounted by cwd/profile”. */
|
|
5
|
+
export declare function renderWorkspaceOpenDocs(nodeId: string): string;
|