cascade-ai 0.48.1 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- package/dist/cli.cjs +2113 -51
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2113 -51
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +2050 -47
- package/dist/index.cjs +2050 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +159 -0
- package/dist/index.d.ts +159 -0
- package/dist/index.js +2050 -47
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -17,6 +17,21 @@ interface ModelInfo {
|
|
|
17
17
|
maxOutputTokens: number;
|
|
18
18
|
supportsStreaming: boolean;
|
|
19
19
|
isLocal: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* True when Cascade has NO price for this model — the cost fields above are
|
|
22
|
+
* 0 only because there is nothing to put in them.
|
|
23
|
+
*
|
|
24
|
+
* This exists because 0 is ambiguous: a local Ollama model genuinely costs
|
|
25
|
+
* nothing, while a newly-released cloud preview model that isn't in the
|
|
26
|
+
* pricing dataset costs real money we can't quote. Treating the second like
|
|
27
|
+
* the first made unknown-priced models report $0.00 spend, put them outside
|
|
28
|
+
* the reach of the per-run cost cap, and gave them the best possible score in
|
|
29
|
+
* the value-based rankers. Anything that reads the cost fields must consult
|
|
30
|
+
* this flag first (see `isPricingUnknown` in core/router/pricing.ts).
|
|
31
|
+
*
|
|
32
|
+
* Never set on local models — their $0 is real.
|
|
33
|
+
*/
|
|
34
|
+
pricingUnknown?: boolean;
|
|
20
35
|
minSizeB?: number;
|
|
21
36
|
/** Tool-use capability. False for Ollama; true for all cloud providers. */
|
|
22
37
|
supportsToolUse?: boolean;
|
|
@@ -46,6 +61,25 @@ interface ProviderConfig {
|
|
|
46
61
|
authToken?: string;
|
|
47
62
|
/** Where an adopted credential came from, e.g. "Claude Code". Informational. */
|
|
48
63
|
credentialSource?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Local-vs-hosted toggle. `true` declares "this endpoint runs on hardware I
|
|
66
|
+
* already pay for, so inference genuinely costs $0"; `false` declares it is a
|
|
67
|
+
* paid hosted endpoint, so an unpriced model there reports "cost not tracked"
|
|
68
|
+
* rather than free.
|
|
69
|
+
*
|
|
70
|
+
* Only meaningful for `ollama` and `openai-compatible`, which can each point
|
|
71
|
+
* at either. Left unset, it defaults sensibly: ollama ⇒ local, and
|
|
72
|
+
* openai-compatible ⇒ local when `baseUrl` is loopback/LAN (llama.cpp, LM
|
|
73
|
+
* Studio, vLLM on your own box) and hosted otherwise (Together, Groq, …).
|
|
74
|
+
* See `isLocalEndpoint` in core/router/pricing.ts.
|
|
75
|
+
*/
|
|
76
|
+
local?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Deployment region for providers whose price varies by region (Azure lists
|
|
79
|
+
* different rates for `global`, `us` and `eu` deployments of the same model).
|
|
80
|
+
* Matched against the pricing dataset's per-region entries.
|
|
81
|
+
*/
|
|
82
|
+
region?: string;
|
|
49
83
|
}
|
|
50
84
|
interface StreamChunk {
|
|
51
85
|
text: string;
|
|
@@ -57,6 +91,13 @@ interface TokenUsage {
|
|
|
57
91
|
outputTokens: number;
|
|
58
92
|
totalTokens: number;
|
|
59
93
|
estimatedCostUsd: number;
|
|
94
|
+
/**
|
|
95
|
+
* True when `estimatedCostUsd` is 0 because the model's price is UNKNOWN, not
|
|
96
|
+
* because the call was free. Cost readouts must say "not tracked" for these
|
|
97
|
+
* rather than "$0.00", and budget code must know the spend it just recorded
|
|
98
|
+
* is an undercount.
|
|
99
|
+
*/
|
|
100
|
+
costUnknown?: boolean;
|
|
60
101
|
}
|
|
61
102
|
interface GenerateOptions {
|
|
62
103
|
messages: ConversationMessage[];
|
|
@@ -1095,6 +1136,25 @@ interface DelegationSavings {
|
|
|
1095
1136
|
|
|
1096
1137
|
type BenchmarkProfile = Partial<Record<Exclude<TaskType, 'mixed'>, number>>;
|
|
1097
1138
|
|
|
1139
|
+
interface PriceDisagreement {
|
|
1140
|
+
modelId: string;
|
|
1141
|
+
provider: string;
|
|
1142
|
+
/** Per-1k rates the bundled dataset asserts. */
|
|
1143
|
+
dataset: {
|
|
1144
|
+
input: number;
|
|
1145
|
+
output: number;
|
|
1146
|
+
};
|
|
1147
|
+
/** Per-1k rates the live source asserts. */
|
|
1148
|
+
live: {
|
|
1149
|
+
input: number;
|
|
1150
|
+
output: number;
|
|
1151
|
+
};
|
|
1152
|
+
/** live/dataset ratio on the blended rate; >1 means live is more expensive. */
|
|
1153
|
+
ratio: number;
|
|
1154
|
+
datasetAsOf: string;
|
|
1155
|
+
datasetSource: string;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1098
1158
|
type DataSource = 'live' | 'cache' | 'bundled';
|
|
1099
1159
|
interface PriceEntry {
|
|
1100
1160
|
input: number;
|
|
@@ -1126,6 +1186,8 @@ declare class LiveDataProvider {
|
|
|
1126
1186
|
private snapshot;
|
|
1127
1187
|
private prices;
|
|
1128
1188
|
private capabilities;
|
|
1189
|
+
/** Dataset-vs-live price mismatches, deduped by `provider:modelId`. */
|
|
1190
|
+
private disagreements;
|
|
1129
1191
|
private source;
|
|
1130
1192
|
private fetchedAt;
|
|
1131
1193
|
private loaded;
|
|
@@ -1156,8 +1218,25 @@ declare class LiveDataProvider {
|
|
|
1156
1218
|
/**
|
|
1157
1219
|
* Returns a price-corrected copy of each model when live pricing is known,
|
|
1158
1220
|
* leaving the original untouched (so the shared catalog is never mutated).
|
|
1221
|
+
*
|
|
1222
|
+
* Reconciliation policy (see reconcilePrice in pricing.ts): the bundled
|
|
1223
|
+
* dataset is the authoritative baseline, and a live quote overrides it
|
|
1224
|
+
* because it is fresher by construction — but the disagreement is RECORDED,
|
|
1225
|
+
* not swallowed. A dataset/live mismatch is the best signal available that
|
|
1226
|
+
* the committed prices have gone stale, so it must be visible rather than
|
|
1227
|
+
* silently resolved in either direction. A live quote of $0 for a non-local
|
|
1228
|
+
* model is rejected outright: that is the very failure mode this change
|
|
1229
|
+
* exists to prevent, and a marketplace's free-tier alias must never zero out
|
|
1230
|
+
* the price of the paid model Cascade is actually calling.
|
|
1159
1231
|
*/
|
|
1160
1232
|
applyLivePricing(models: ModelInfo[]): ModelInfo[];
|
|
1233
|
+
private recordDisagreement;
|
|
1234
|
+
/**
|
|
1235
|
+
* Dataset-vs-live price mismatches seen this session, newest-model-first
|
|
1236
|
+
* insertion order. Surfaced by `cascade models --verbose`; a non-empty list
|
|
1237
|
+
* means src/core/router/pricing-data.json is probably due a refresh.
|
|
1238
|
+
*/
|
|
1239
|
+
getPriceDisagreements(): PriceDisagreement[];
|
|
1161
1240
|
/** Current capability facts for a model id, or null when unknown. */
|
|
1162
1241
|
getCapability(modelId: string): CapabilityEntry | null;
|
|
1163
1242
|
/**
|
|
@@ -1345,6 +1424,14 @@ interface RouterStats {
|
|
|
1345
1424
|
outputTokensByTier: Record<string, number>;
|
|
1346
1425
|
/** Accumulated cost (USD) broken down by feature tag (e.g. T2 section names). */
|
|
1347
1426
|
costByFeature: Record<string, number>;
|
|
1427
|
+
/**
|
|
1428
|
+
* Calls made against models Cascade has no price for. Their spend is NOT in
|
|
1429
|
+
* `totalCostUsd` — it can't be — so a non-zero count here means the totals
|
|
1430
|
+
* above (and any budget check derived from them) are an undercount.
|
|
1431
|
+
*/
|
|
1432
|
+
untrackedCostCalls: number;
|
|
1433
|
+
/** Ids of the models responsible, for "cost not tracked for: …" readouts. */
|
|
1434
|
+
untrackedCostModels: string[];
|
|
1348
1435
|
}
|
|
1349
1436
|
declare class CascadeRouter extends EventEmitter {
|
|
1350
1437
|
private selector;
|
|
@@ -1771,6 +1858,12 @@ declare abstract class BaseTier extends EventEmitter {
|
|
|
1771
1858
|
getStatus(): TierStatus;
|
|
1772
1859
|
protected setStatus(status: TierStatus, output?: string): void;
|
|
1773
1860
|
/** Record the model serving this tier; future status events carry it. */
|
|
1861
|
+
/**
|
|
1862
|
+
* The model actually serving this tier ("provider:id"), once one has been
|
|
1863
|
+
* chosen. The run breaker reads it to attribute a failure to a model rather
|
|
1864
|
+
* than to the subtask that happened to hit it.
|
|
1865
|
+
*/
|
|
1866
|
+
getServingModel(): string | undefined;
|
|
1774
1867
|
protected setServingModel(model: string | undefined): void;
|
|
1775
1868
|
protected setLabel(label: string): void;
|
|
1776
1869
|
setSystemPromptOverride(prompt: string): void;
|
|
@@ -1786,6 +1879,61 @@ declare abstract class BaseTier extends EventEmitter {
|
|
|
1786
1879
|
protected throwIfCancelled(): void;
|
|
1787
1880
|
}
|
|
1788
1881
|
|
|
1882
|
+
/**
|
|
1883
|
+
* What kind of failure this is, and — crucially — whether retrying a DIFFERENT
|
|
1884
|
+
* subtask against the same model could plausibly succeed.
|
|
1885
|
+
*/
|
|
1886
|
+
type ProviderErrorKind = 'rate_limit' | 'quota_exhausted' | 'auth' | 'model_unavailable' | 'context_length' | 'content_filter' | 'network' | 'unknown';
|
|
1887
|
+
interface ClassifiedError {
|
|
1888
|
+
kind: ProviderErrorKind;
|
|
1889
|
+
/** True when every other worker on this model is expected to fail the same way. */
|
|
1890
|
+
systemic: boolean;
|
|
1891
|
+
/** HTTP status, when the provider gave us one. */
|
|
1892
|
+
status?: number;
|
|
1893
|
+
/** The provider's own message, trimmed — never invented or paraphrased. */
|
|
1894
|
+
raw: string;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
interface BreakerTrip {
|
|
1898
|
+
modelId: string;
|
|
1899
|
+
kind: ClassifiedError['kind'];
|
|
1900
|
+
/** How many systemic failures on this model were seen before tripping. */
|
|
1901
|
+
failures: number;
|
|
1902
|
+
/** User-facing explanation, including the provider's own words. */
|
|
1903
|
+
message: string;
|
|
1904
|
+
}
|
|
1905
|
+
declare class RunBreaker {
|
|
1906
|
+
private readonly threshold;
|
|
1907
|
+
/** modelId → consecutive systemic failures of the SAME kind. */
|
|
1908
|
+
private counts;
|
|
1909
|
+
private trip;
|
|
1910
|
+
constructor(threshold?: number);
|
|
1911
|
+
/**
|
|
1912
|
+
* Record a failed model call. Returns the trip if this failure is the one
|
|
1913
|
+
* that opened the breaker, otherwise null.
|
|
1914
|
+
*
|
|
1915
|
+
* A non-systemic failure RESETS the model's counter: it is evidence the model
|
|
1916
|
+
* is reachable and answering, which is exactly what a systemic streak claims
|
|
1917
|
+
* it is not.
|
|
1918
|
+
*/
|
|
1919
|
+
record(err: unknown, modelId: string | undefined): BreakerTrip | null;
|
|
1920
|
+
/** True once the run should stop launching work that would fail identically. */
|
|
1921
|
+
isOpen(): boolean;
|
|
1922
|
+
/** Why the breaker opened, or null while it is closed. */
|
|
1923
|
+
reason(): BreakerTrip | null;
|
|
1924
|
+
/**
|
|
1925
|
+
* One line for a worker/section that never ran because the breaker was
|
|
1926
|
+
* already open. Says the work was skipped deliberately — not that it failed
|
|
1927
|
+
* on its own merits, which is what an empty FAILED status would imply.
|
|
1928
|
+
*/
|
|
1929
|
+
skipMessage(): string;
|
|
1930
|
+
/** Test/diagnostic view of the per-model streaks. */
|
|
1931
|
+
streaks(): Record<string, {
|
|
1932
|
+
kind: string;
|
|
1933
|
+
n: number;
|
|
1934
|
+
}>;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1789
1937
|
type T2Evaluator = (req: PermissionRequest) => Promise<PermissionDecision | null>;
|
|
1790
1938
|
type T1Evaluator = (req: PermissionRequest) => Promise<PermissionDecision | null>;
|
|
1791
1939
|
/**
|
|
@@ -1933,6 +2081,7 @@ declare class T1Administrator extends BaseTier {
|
|
|
1933
2081
|
private t2Managers;
|
|
1934
2082
|
private escalations;
|
|
1935
2083
|
private store?;
|
|
2084
|
+
private runBreaker?;
|
|
1936
2085
|
private t2PeerBus;
|
|
1937
2086
|
private permissionEscalator?;
|
|
1938
2087
|
private toolCreator?;
|
|
@@ -1942,6 +2091,8 @@ declare class T1Administrator extends BaseTier {
|
|
|
1942
2091
|
private peerMessageSessionId;
|
|
1943
2092
|
private planApprovalCallback?;
|
|
1944
2093
|
constructor(router: CascadeRouter, toolRegistry: ToolRegistry, config: CascadeConfig);
|
|
2094
|
+
/** Share the run-wide circuit breaker with every section manager. */
|
|
2095
|
+
setRunBreaker(breaker: RunBreaker): void;
|
|
1945
2096
|
setStore(store: MemoryStore): void;
|
|
1946
2097
|
/**
|
|
1947
2098
|
* Inject the shared PermissionEscalator for this task run.
|
|
@@ -2317,6 +2468,7 @@ declare class T2Manager extends BaseTier {
|
|
|
2317
2468
|
private escalations;
|
|
2318
2469
|
private peerSyncBuffer;
|
|
2319
2470
|
private store?;
|
|
2471
|
+
private runBreaker?;
|
|
2320
2472
|
private t3PeerBus;
|
|
2321
2473
|
private t2PeerBus?;
|
|
2322
2474
|
private permissionEscalator?;
|
|
@@ -2328,6 +2480,13 @@ declare class T2Manager extends BaseTier {
|
|
|
2328
2480
|
setPeerBus(bus: PeerBus): void;
|
|
2329
2481
|
setPeerMessageCallback(cb: (event: PeerMessageEvent) => void, sessionId: string): void;
|
|
2330
2482
|
constructor(router: CascadeRouter, toolRegistry: ToolRegistry, parentId: string);
|
|
2483
|
+
/**
|
|
2484
|
+
* Share the run-wide circuit breaker. Every T2 in a run gets the SAME
|
|
2485
|
+
* instance: three sections each failing once against a dead model is the
|
|
2486
|
+
* same fact as one section failing three times, and a per-section breaker
|
|
2487
|
+
* would never see it.
|
|
2488
|
+
*/
|
|
2489
|
+
setRunBreaker(breaker: RunBreaker): void;
|
|
2331
2490
|
setStore(store: MemoryStore): void;
|
|
2332
2491
|
/**
|
|
2333
2492
|
* Inject the shared PermissionEscalator for this task run.
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,21 @@ interface ModelInfo {
|
|
|
17
17
|
maxOutputTokens: number;
|
|
18
18
|
supportsStreaming: boolean;
|
|
19
19
|
isLocal: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* True when Cascade has NO price for this model — the cost fields above are
|
|
22
|
+
* 0 only because there is nothing to put in them.
|
|
23
|
+
*
|
|
24
|
+
* This exists because 0 is ambiguous: a local Ollama model genuinely costs
|
|
25
|
+
* nothing, while a newly-released cloud preview model that isn't in the
|
|
26
|
+
* pricing dataset costs real money we can't quote. Treating the second like
|
|
27
|
+
* the first made unknown-priced models report $0.00 spend, put them outside
|
|
28
|
+
* the reach of the per-run cost cap, and gave them the best possible score in
|
|
29
|
+
* the value-based rankers. Anything that reads the cost fields must consult
|
|
30
|
+
* this flag first (see `isPricingUnknown` in core/router/pricing.ts).
|
|
31
|
+
*
|
|
32
|
+
* Never set on local models — their $0 is real.
|
|
33
|
+
*/
|
|
34
|
+
pricingUnknown?: boolean;
|
|
20
35
|
minSizeB?: number;
|
|
21
36
|
/** Tool-use capability. False for Ollama; true for all cloud providers. */
|
|
22
37
|
supportsToolUse?: boolean;
|
|
@@ -46,6 +61,25 @@ interface ProviderConfig {
|
|
|
46
61
|
authToken?: string;
|
|
47
62
|
/** Where an adopted credential came from, e.g. "Claude Code". Informational. */
|
|
48
63
|
credentialSource?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Local-vs-hosted toggle. `true` declares "this endpoint runs on hardware I
|
|
66
|
+
* already pay for, so inference genuinely costs $0"; `false` declares it is a
|
|
67
|
+
* paid hosted endpoint, so an unpriced model there reports "cost not tracked"
|
|
68
|
+
* rather than free.
|
|
69
|
+
*
|
|
70
|
+
* Only meaningful for `ollama` and `openai-compatible`, which can each point
|
|
71
|
+
* at either. Left unset, it defaults sensibly: ollama ⇒ local, and
|
|
72
|
+
* openai-compatible ⇒ local when `baseUrl` is loopback/LAN (llama.cpp, LM
|
|
73
|
+
* Studio, vLLM on your own box) and hosted otherwise (Together, Groq, …).
|
|
74
|
+
* See `isLocalEndpoint` in core/router/pricing.ts.
|
|
75
|
+
*/
|
|
76
|
+
local?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Deployment region for providers whose price varies by region (Azure lists
|
|
79
|
+
* different rates for `global`, `us` and `eu` deployments of the same model).
|
|
80
|
+
* Matched against the pricing dataset's per-region entries.
|
|
81
|
+
*/
|
|
82
|
+
region?: string;
|
|
49
83
|
}
|
|
50
84
|
interface StreamChunk {
|
|
51
85
|
text: string;
|
|
@@ -57,6 +91,13 @@ interface TokenUsage {
|
|
|
57
91
|
outputTokens: number;
|
|
58
92
|
totalTokens: number;
|
|
59
93
|
estimatedCostUsd: number;
|
|
94
|
+
/**
|
|
95
|
+
* True when `estimatedCostUsd` is 0 because the model's price is UNKNOWN, not
|
|
96
|
+
* because the call was free. Cost readouts must say "not tracked" for these
|
|
97
|
+
* rather than "$0.00", and budget code must know the spend it just recorded
|
|
98
|
+
* is an undercount.
|
|
99
|
+
*/
|
|
100
|
+
costUnknown?: boolean;
|
|
60
101
|
}
|
|
61
102
|
interface GenerateOptions {
|
|
62
103
|
messages: ConversationMessage[];
|
|
@@ -1095,6 +1136,25 @@ interface DelegationSavings {
|
|
|
1095
1136
|
|
|
1096
1137
|
type BenchmarkProfile = Partial<Record<Exclude<TaskType, 'mixed'>, number>>;
|
|
1097
1138
|
|
|
1139
|
+
interface PriceDisagreement {
|
|
1140
|
+
modelId: string;
|
|
1141
|
+
provider: string;
|
|
1142
|
+
/** Per-1k rates the bundled dataset asserts. */
|
|
1143
|
+
dataset: {
|
|
1144
|
+
input: number;
|
|
1145
|
+
output: number;
|
|
1146
|
+
};
|
|
1147
|
+
/** Per-1k rates the live source asserts. */
|
|
1148
|
+
live: {
|
|
1149
|
+
input: number;
|
|
1150
|
+
output: number;
|
|
1151
|
+
};
|
|
1152
|
+
/** live/dataset ratio on the blended rate; >1 means live is more expensive. */
|
|
1153
|
+
ratio: number;
|
|
1154
|
+
datasetAsOf: string;
|
|
1155
|
+
datasetSource: string;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1098
1158
|
type DataSource = 'live' | 'cache' | 'bundled';
|
|
1099
1159
|
interface PriceEntry {
|
|
1100
1160
|
input: number;
|
|
@@ -1126,6 +1186,8 @@ declare class LiveDataProvider {
|
|
|
1126
1186
|
private snapshot;
|
|
1127
1187
|
private prices;
|
|
1128
1188
|
private capabilities;
|
|
1189
|
+
/** Dataset-vs-live price mismatches, deduped by `provider:modelId`. */
|
|
1190
|
+
private disagreements;
|
|
1129
1191
|
private source;
|
|
1130
1192
|
private fetchedAt;
|
|
1131
1193
|
private loaded;
|
|
@@ -1156,8 +1218,25 @@ declare class LiveDataProvider {
|
|
|
1156
1218
|
/**
|
|
1157
1219
|
* Returns a price-corrected copy of each model when live pricing is known,
|
|
1158
1220
|
* leaving the original untouched (so the shared catalog is never mutated).
|
|
1221
|
+
*
|
|
1222
|
+
* Reconciliation policy (see reconcilePrice in pricing.ts): the bundled
|
|
1223
|
+
* dataset is the authoritative baseline, and a live quote overrides it
|
|
1224
|
+
* because it is fresher by construction — but the disagreement is RECORDED,
|
|
1225
|
+
* not swallowed. A dataset/live mismatch is the best signal available that
|
|
1226
|
+
* the committed prices have gone stale, so it must be visible rather than
|
|
1227
|
+
* silently resolved in either direction. A live quote of $0 for a non-local
|
|
1228
|
+
* model is rejected outright: that is the very failure mode this change
|
|
1229
|
+
* exists to prevent, and a marketplace's free-tier alias must never zero out
|
|
1230
|
+
* the price of the paid model Cascade is actually calling.
|
|
1159
1231
|
*/
|
|
1160
1232
|
applyLivePricing(models: ModelInfo[]): ModelInfo[];
|
|
1233
|
+
private recordDisagreement;
|
|
1234
|
+
/**
|
|
1235
|
+
* Dataset-vs-live price mismatches seen this session, newest-model-first
|
|
1236
|
+
* insertion order. Surfaced by `cascade models --verbose`; a non-empty list
|
|
1237
|
+
* means src/core/router/pricing-data.json is probably due a refresh.
|
|
1238
|
+
*/
|
|
1239
|
+
getPriceDisagreements(): PriceDisagreement[];
|
|
1161
1240
|
/** Current capability facts for a model id, or null when unknown. */
|
|
1162
1241
|
getCapability(modelId: string): CapabilityEntry | null;
|
|
1163
1242
|
/**
|
|
@@ -1345,6 +1424,14 @@ interface RouterStats {
|
|
|
1345
1424
|
outputTokensByTier: Record<string, number>;
|
|
1346
1425
|
/** Accumulated cost (USD) broken down by feature tag (e.g. T2 section names). */
|
|
1347
1426
|
costByFeature: Record<string, number>;
|
|
1427
|
+
/**
|
|
1428
|
+
* Calls made against models Cascade has no price for. Their spend is NOT in
|
|
1429
|
+
* `totalCostUsd` — it can't be — so a non-zero count here means the totals
|
|
1430
|
+
* above (and any budget check derived from them) are an undercount.
|
|
1431
|
+
*/
|
|
1432
|
+
untrackedCostCalls: number;
|
|
1433
|
+
/** Ids of the models responsible, for "cost not tracked for: …" readouts. */
|
|
1434
|
+
untrackedCostModels: string[];
|
|
1348
1435
|
}
|
|
1349
1436
|
declare class CascadeRouter extends EventEmitter {
|
|
1350
1437
|
private selector;
|
|
@@ -1771,6 +1858,12 @@ declare abstract class BaseTier extends EventEmitter {
|
|
|
1771
1858
|
getStatus(): TierStatus;
|
|
1772
1859
|
protected setStatus(status: TierStatus, output?: string): void;
|
|
1773
1860
|
/** Record the model serving this tier; future status events carry it. */
|
|
1861
|
+
/**
|
|
1862
|
+
* The model actually serving this tier ("provider:id"), once one has been
|
|
1863
|
+
* chosen. The run breaker reads it to attribute a failure to a model rather
|
|
1864
|
+
* than to the subtask that happened to hit it.
|
|
1865
|
+
*/
|
|
1866
|
+
getServingModel(): string | undefined;
|
|
1774
1867
|
protected setServingModel(model: string | undefined): void;
|
|
1775
1868
|
protected setLabel(label: string): void;
|
|
1776
1869
|
setSystemPromptOverride(prompt: string): void;
|
|
@@ -1786,6 +1879,61 @@ declare abstract class BaseTier extends EventEmitter {
|
|
|
1786
1879
|
protected throwIfCancelled(): void;
|
|
1787
1880
|
}
|
|
1788
1881
|
|
|
1882
|
+
/**
|
|
1883
|
+
* What kind of failure this is, and — crucially — whether retrying a DIFFERENT
|
|
1884
|
+
* subtask against the same model could plausibly succeed.
|
|
1885
|
+
*/
|
|
1886
|
+
type ProviderErrorKind = 'rate_limit' | 'quota_exhausted' | 'auth' | 'model_unavailable' | 'context_length' | 'content_filter' | 'network' | 'unknown';
|
|
1887
|
+
interface ClassifiedError {
|
|
1888
|
+
kind: ProviderErrorKind;
|
|
1889
|
+
/** True when every other worker on this model is expected to fail the same way. */
|
|
1890
|
+
systemic: boolean;
|
|
1891
|
+
/** HTTP status, when the provider gave us one. */
|
|
1892
|
+
status?: number;
|
|
1893
|
+
/** The provider's own message, trimmed — never invented or paraphrased. */
|
|
1894
|
+
raw: string;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
interface BreakerTrip {
|
|
1898
|
+
modelId: string;
|
|
1899
|
+
kind: ClassifiedError['kind'];
|
|
1900
|
+
/** How many systemic failures on this model were seen before tripping. */
|
|
1901
|
+
failures: number;
|
|
1902
|
+
/** User-facing explanation, including the provider's own words. */
|
|
1903
|
+
message: string;
|
|
1904
|
+
}
|
|
1905
|
+
declare class RunBreaker {
|
|
1906
|
+
private readonly threshold;
|
|
1907
|
+
/** modelId → consecutive systemic failures of the SAME kind. */
|
|
1908
|
+
private counts;
|
|
1909
|
+
private trip;
|
|
1910
|
+
constructor(threshold?: number);
|
|
1911
|
+
/**
|
|
1912
|
+
* Record a failed model call. Returns the trip if this failure is the one
|
|
1913
|
+
* that opened the breaker, otherwise null.
|
|
1914
|
+
*
|
|
1915
|
+
* A non-systemic failure RESETS the model's counter: it is evidence the model
|
|
1916
|
+
* is reachable and answering, which is exactly what a systemic streak claims
|
|
1917
|
+
* it is not.
|
|
1918
|
+
*/
|
|
1919
|
+
record(err: unknown, modelId: string | undefined): BreakerTrip | null;
|
|
1920
|
+
/** True once the run should stop launching work that would fail identically. */
|
|
1921
|
+
isOpen(): boolean;
|
|
1922
|
+
/** Why the breaker opened, or null while it is closed. */
|
|
1923
|
+
reason(): BreakerTrip | null;
|
|
1924
|
+
/**
|
|
1925
|
+
* One line for a worker/section that never ran because the breaker was
|
|
1926
|
+
* already open. Says the work was skipped deliberately — not that it failed
|
|
1927
|
+
* on its own merits, which is what an empty FAILED status would imply.
|
|
1928
|
+
*/
|
|
1929
|
+
skipMessage(): string;
|
|
1930
|
+
/** Test/diagnostic view of the per-model streaks. */
|
|
1931
|
+
streaks(): Record<string, {
|
|
1932
|
+
kind: string;
|
|
1933
|
+
n: number;
|
|
1934
|
+
}>;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1789
1937
|
type T2Evaluator = (req: PermissionRequest) => Promise<PermissionDecision | null>;
|
|
1790
1938
|
type T1Evaluator = (req: PermissionRequest) => Promise<PermissionDecision | null>;
|
|
1791
1939
|
/**
|
|
@@ -1933,6 +2081,7 @@ declare class T1Administrator extends BaseTier {
|
|
|
1933
2081
|
private t2Managers;
|
|
1934
2082
|
private escalations;
|
|
1935
2083
|
private store?;
|
|
2084
|
+
private runBreaker?;
|
|
1936
2085
|
private t2PeerBus;
|
|
1937
2086
|
private permissionEscalator?;
|
|
1938
2087
|
private toolCreator?;
|
|
@@ -1942,6 +2091,8 @@ declare class T1Administrator extends BaseTier {
|
|
|
1942
2091
|
private peerMessageSessionId;
|
|
1943
2092
|
private planApprovalCallback?;
|
|
1944
2093
|
constructor(router: CascadeRouter, toolRegistry: ToolRegistry, config: CascadeConfig);
|
|
2094
|
+
/** Share the run-wide circuit breaker with every section manager. */
|
|
2095
|
+
setRunBreaker(breaker: RunBreaker): void;
|
|
1945
2096
|
setStore(store: MemoryStore): void;
|
|
1946
2097
|
/**
|
|
1947
2098
|
* Inject the shared PermissionEscalator for this task run.
|
|
@@ -2317,6 +2468,7 @@ declare class T2Manager extends BaseTier {
|
|
|
2317
2468
|
private escalations;
|
|
2318
2469
|
private peerSyncBuffer;
|
|
2319
2470
|
private store?;
|
|
2471
|
+
private runBreaker?;
|
|
2320
2472
|
private t3PeerBus;
|
|
2321
2473
|
private t2PeerBus?;
|
|
2322
2474
|
private permissionEscalator?;
|
|
@@ -2328,6 +2480,13 @@ declare class T2Manager extends BaseTier {
|
|
|
2328
2480
|
setPeerBus(bus: PeerBus): void;
|
|
2329
2481
|
setPeerMessageCallback(cb: (event: PeerMessageEvent) => void, sessionId: string): void;
|
|
2330
2482
|
constructor(router: CascadeRouter, toolRegistry: ToolRegistry, parentId: string);
|
|
2483
|
+
/**
|
|
2484
|
+
* Share the run-wide circuit breaker. Every T2 in a run gets the SAME
|
|
2485
|
+
* instance: three sections each failing once against a dead model is the
|
|
2486
|
+
* same fact as one section failing three times, and a per-section breaker
|
|
2487
|
+
* would never see it.
|
|
2488
|
+
*/
|
|
2489
|
+
setRunBreaker(breaker: RunBreaker): void;
|
|
2331
2490
|
setStore(store: MemoryStore): void;
|
|
2332
2491
|
/**
|
|
2333
2492
|
* Inject the shared PermissionEscalator for this task run.
|