@oeronteros-1/opencode-orchestra 1.0.28 → 1.0.30
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 +79 -1
- package/dashboard-dist/assets/index-Bl2J2mOP.js +132 -0
- package/dashboard-dist/assets/index-CX2uv1EJ.css +1 -0
- package/dashboard-dist/index.html +2 -2
- package/dist/agents/integrator.js +1 -1
- package/dist/agents/integrator.js.map +1 -1
- package/dist/agents/lead.js +2 -13
- package/dist/agents/lead.js.map +1 -1
- package/dist/config/load.d.ts +5 -0
- package/dist/config/load.js +20 -5
- package/dist/config/load.js.map +1 -1
- package/dist/dashboard/server.js +38 -17
- package/dist/dashboard/server.js.map +1 -1
- package/dist/diagnostics/doctor.js +194 -0
- package/dist/diagnostics/doctor.js.map +1 -1
- package/dist/diagnostics/update.js +8 -7
- package/dist/diagnostics/update.js.map +1 -1
- package/dist/index.js +21 -3
- package/dist/index.js.map +1 -1
- package/dist/orchestration/ownership.d.ts +19 -0
- package/dist/orchestration/ownership.js +26 -0
- package/dist/orchestration/ownership.js.map +1 -1
- package/dist/routing/classifier.js +20 -2
- package/dist/routing/classifier.js.map +1 -1
- package/dist/routing/fallback.d.ts +19 -10
- package/dist/routing/fallback.js +89 -18
- package/dist/routing/fallback.js.map +1 -1
- package/dist/routing/model-resolver.d.ts +47 -0
- package/dist/routing/model-resolver.js +110 -2
- package/dist/routing/model-resolver.js.map +1 -1
- package/dist/spawn.d.ts +14 -1
- package/dist/spawn.js +67 -4
- package/dist/spawn.js.map +1 -1
- package/dist/telemetry/ledger.d.ts +49 -0
- package/dist/telemetry/ledger.js +157 -0
- package/dist/telemetry/ledger.js.map +1 -1
- package/dist/telemetry/live.js +1 -1
- package/dist/telemetry/live.js.map +1 -1
- package/dist/tools.js +71 -3
- package/dist/tools.js.map +1 -1
- package/package.json +4 -4
- package/dashboard-dist/assets/index-BTleCKc7.js +0 -124
- package/dashboard-dist/assets/index-igZBUoKO.css +0 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classifier.js","sourceRoot":"","sources":["../../src/routing/classifier.ts"],"names":[],"mappings":"AAaA,MAAM,OAAO,GAAkC;IAC7C,YAAY,EAAE;QACZ,cAAc;QACd,WAAW;QACX,QAAQ;QACR,QAAQ;QACR,UAAU;QACV,KAAK;QACL,UAAU;QACV,SAAS;QACT,aAAa;QACb,UAAU;QACV,SAAS;QACT,OAAO;QACP,UAAU;KACX;IACD,KAAK,EAAE;QACL,KAAK;QACL,OAAO;QACP,OAAO;QACP,WAAW;QACX,OAAO;QACP,QAAQ;QACR,KAAK;QACL,OAAO;QACP,gBAAgB;QAChB,MAAM;QACN,QAAQ;QACR,aAAa;QACb,UAAU;KACX;IACD,EAAE,EAAE;QACF,IAAI;QACJ,IAAI;QACJ,QAAQ;QACR,WAAW;QACX,YAAY;QACZ,WAAW;QACX,QAAQ;QACR,WAAW;QACX,WAAW;QACX,QAAQ;QACR,QAAQ;QACR,SAAS;KACV;IACD,QAAQ,EAAE;QACR,UAAU;QACV,SAAS;QACT,aAAa;QACb,eAAe;QACf,eAAe;QACf,OAAO;QACP,QAAQ;QACR,QAAQ;QACR,eAAe;QACf,YAAY;KACb;IACD,MAAM,EAAE;QACN,QAAQ;QACR,YAAY;QACZ,cAAc;QACd,MAAM;QACN,cAAc;QACd,OAAO;QACP,aAAa;QACb,iBAAiB;KAClB;IACD,QAAQ,EAAE;QACR,UAAU;QACV,eAAe;QACf,MAAM;QACN,OAAO;QACP,OAAO;QACP,QAAQ;QACR,KAAK;QACL,WAAW;QACX,YAAY;QACZ,SAAS;QACT,QAAQ;QACR,WAAW;KACZ;IACD,WAAW,EAAE;QACX,aAAa;QACb,SAAS;QACT,MAAM;QACN,QAAQ;QACR,KAAK;QACL,YAAY;QACZ,UAAU;QACV,eAAe;QACf,QAAQ;QACR,SAAS;KACV;IACD,SAAS,EAAE;QACT,WAAW;QACX,SAAS;QACT,SAAS;QACT,SAAS;QACT,
|
|
1
|
+
{"version":3,"file":"classifier.js","sourceRoot":"","sources":["../../src/routing/classifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,33 +1,42 @@
|
|
|
1
|
-
import type { BudgetMode, ModelCandidateInput, ModelCost } from "../config/schema.js";
|
|
1
|
+
import type { BudgetMode, ModelCandidateInput, ModelCost, ModelTier } from "../config/schema.js";
|
|
2
2
|
import { resolveModel } from "./model-resolver.js";
|
|
3
3
|
export interface FallbackEntry {
|
|
4
4
|
id: string;
|
|
5
5
|
cost: ModelCost;
|
|
6
|
-
/**
|
|
6
|
+
/** Static cost-class rank: paid (2) > subscription (1) > free (0). */
|
|
7
7
|
costRank: number;
|
|
8
|
+
/** True when the capability is declared or explicitly scored; false when unknown. */
|
|
9
|
+
compatible: boolean;
|
|
10
|
+
priority: number;
|
|
11
|
+
tier: ModelTier;
|
|
8
12
|
}
|
|
9
13
|
export interface FallbackChain {
|
|
10
14
|
/** The winning model id (first choice). */
|
|
11
15
|
primary: string;
|
|
12
|
-
/** Ordered
|
|
16
|
+
/** Ordered alternatives after the primary (compatibility first). */
|
|
13
17
|
alternatives: FallbackEntry[];
|
|
14
18
|
/** All entries (primary + alternatives) in failover order. */
|
|
15
19
|
all: FallbackEntry[];
|
|
16
20
|
}
|
|
21
|
+
export type ErrorKind = "rate-limit" | "server" | "timeout" | "auth" | "invalid-request" | "other";
|
|
17
22
|
interface RetryableError {
|
|
18
|
-
/**
|
|
19
|
-
kind:
|
|
23
|
+
/** Retryable failures trigger failover; auth and invalid-request are terminal. */
|
|
24
|
+
kind: ErrorKind;
|
|
20
25
|
}
|
|
21
26
|
/**
|
|
22
|
-
* Classify an error into
|
|
23
|
-
*
|
|
27
|
+
* Classify an error into policy categories. Only rate-limit, timeout, and
|
|
28
|
+
* server failures are retryable; auth and invalid-request stop the chain.
|
|
24
29
|
*/
|
|
25
30
|
export declare function classifyError(error: unknown): RetryableError;
|
|
26
31
|
export declare function isRetryable(kind: RetryableError["kind"]): boolean;
|
|
27
32
|
/**
|
|
28
33
|
* Build a failover chain for a capability pool. The primary is the current
|
|
29
|
-
* winner; alternatives are the remaining candidates
|
|
30
|
-
*
|
|
34
|
+
* winner; alternatives are the remaining candidates ranked deterministically:
|
|
35
|
+
* capability compatibility, then priority, tier, budget-aware cost class, and
|
|
36
|
+
* model id. Explicitly incompatible candidates are excluded before resolution
|
|
37
|
+
* so the primary is always compatible; unknown-capability candidates stay
|
|
38
|
+
* eligible but rank below compatible ones. Budget filters (paid exclusion)
|
|
39
|
+
* apply to alternatives as well as the primary.
|
|
31
40
|
*/
|
|
32
41
|
export declare function buildFallbackChain(pool: ModelCandidateInput[], capability: Parameters<typeof resolveModel>[0]["capability"], budget: BudgetMode, allowPaid: boolean, options?: {
|
|
33
42
|
preferredCosts?: ModelCost[];
|
|
@@ -37,7 +46,7 @@ export declare function buildFallbackChain(pool: ModelCandidateInput[], capabili
|
|
|
37
46
|
}): FallbackChain | undefined;
|
|
38
47
|
/**
|
|
39
48
|
* Given a chain and the id of a model that just failed, return the next
|
|
40
|
-
*
|
|
49
|
+
* alternative to try, or undefined when the chain is exhausted.
|
|
41
50
|
*/
|
|
42
51
|
export declare function nextAfterFailure(chain: FallbackChain, failedId: string): string | undefined;
|
|
43
52
|
export {};
|
package/dist/routing/fallback.js
CHANGED
|
@@ -1,11 +1,39 @@
|
|
|
1
|
-
import { normalizeCandidate, resolveModel } from "./model-resolver.js";
|
|
1
|
+
import { isCapabilityCompatible, isCapabilityIncompatible, normalizeCandidate, resolveModel, } from "./model-resolver.js";
|
|
2
2
|
const COST_RANK = { paid: 2, subscription: 1, free: 0 };
|
|
3
|
+
const TIER_RANK = { frontier: 2, lead: 1, worker: 0 };
|
|
4
|
+
/**
|
|
5
|
+
* Budget-aware cost-class preference applied as the tie-break after priority
|
|
6
|
+
* and tier. `ModelCost` is a closed union, so an unknown cost can never be
|
|
7
|
+
* coerced into a cheaper class.
|
|
8
|
+
*/
|
|
9
|
+
const BUDGET_COST_PREFERENCE = {
|
|
10
|
+
eco: ["free", "subscription", "paid"],
|
|
11
|
+
balanced: ["subscription", "free", "paid"],
|
|
12
|
+
quality: ["subscription", "paid", "free"],
|
|
13
|
+
ebobo: ["paid", "subscription", "free"],
|
|
14
|
+
};
|
|
3
15
|
function compareCodepoints(a, b) {
|
|
4
16
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
5
17
|
}
|
|
18
|
+
function costPreferenceOrder(budget, preferredCosts) {
|
|
19
|
+
if (preferredCosts && preferredCosts.length > 0)
|
|
20
|
+
return preferredCosts;
|
|
21
|
+
return BUDGET_COST_PREFERENCE[budget];
|
|
22
|
+
}
|
|
23
|
+
function costPreferenceRank(cost, order) {
|
|
24
|
+
const index = order.indexOf(cost);
|
|
25
|
+
return index === -1 ? order.length : index;
|
|
26
|
+
}
|
|
27
|
+
function isBudgetEligible(candidate, allowPaid, paidCallsUsed, maxPaidCalls) {
|
|
28
|
+
if (candidate.cost !== "paid")
|
|
29
|
+
return true;
|
|
30
|
+
if (!allowPaid)
|
|
31
|
+
return false;
|
|
32
|
+
return maxPaidCalls === undefined || paidCallsUsed < maxPaidCalls;
|
|
33
|
+
}
|
|
6
34
|
/**
|
|
7
|
-
* Classify an error into
|
|
8
|
-
*
|
|
35
|
+
* Classify an error into policy categories. Only rate-limit, timeout, and
|
|
36
|
+
* server failures are retryable; auth and invalid-request stop the chain.
|
|
9
37
|
*/
|
|
10
38
|
export function classifyError(error) {
|
|
11
39
|
const candidate = typeof error === "object" && error !== null ? error : undefined;
|
|
@@ -18,6 +46,12 @@ export function classifyError(error) {
|
|
|
18
46
|
: String(error);
|
|
19
47
|
const status = candidate?.status ?? candidate?.statusCode;
|
|
20
48
|
const text = (status !== undefined ? String(status) + " " : "") + message.toLowerCase();
|
|
49
|
+
if (status === 401 || status === 403 || text.includes("unauthorized") || text.includes("forbidden") || text.includes("invalid api key") || text.includes("authentication")) {
|
|
50
|
+
return { kind: "auth" };
|
|
51
|
+
}
|
|
52
|
+
if (status === 400 || status === 404 || text.includes("not found") || text.includes("bad request") || text.includes("invalid request")) {
|
|
53
|
+
return { kind: "invalid-request" };
|
|
54
|
+
}
|
|
21
55
|
if (status === 429 || text.includes("429") || text.includes("rate limit") || text.includes("rate-limit") || text.includes("too many requests")) {
|
|
22
56
|
return { kind: "rate-limit" };
|
|
23
57
|
}
|
|
@@ -34,15 +68,25 @@ export function isRetryable(kind) {
|
|
|
34
68
|
}
|
|
35
69
|
/**
|
|
36
70
|
* Build a failover chain for a capability pool. The primary is the current
|
|
37
|
-
* winner; alternatives are the remaining candidates
|
|
38
|
-
*
|
|
71
|
+
* winner; alternatives are the remaining candidates ranked deterministically:
|
|
72
|
+
* capability compatibility, then priority, tier, budget-aware cost class, and
|
|
73
|
+
* model id. Explicitly incompatible candidates are excluded before resolution
|
|
74
|
+
* so the primary is always compatible; unknown-capability candidates stay
|
|
75
|
+
* eligible but rank below compatible ones. Budget filters (paid exclusion)
|
|
76
|
+
* apply to alternatives as well as the primary.
|
|
39
77
|
*/
|
|
40
78
|
export function buildFallbackChain(pool, capability, budget, allowPaid, options = {}) {
|
|
41
79
|
const normalized = pool.map(normalizeCandidate);
|
|
42
80
|
if (normalized.length === 0)
|
|
43
81
|
return undefined;
|
|
82
|
+
// The primary must always be capability-compatible: drop explicitly
|
|
83
|
+
// incompatible candidates before resolution so a high-priority incompatible
|
|
84
|
+
// model (e.g. an ebobo frontier) cannot take the primary slot.
|
|
85
|
+
const compatiblePool = normalized.filter((candidate) => !isCapabilityIncompatible(candidate, capability));
|
|
86
|
+
if (compatiblePool.length === 0)
|
|
87
|
+
return undefined;
|
|
44
88
|
const winner = resolveModel({
|
|
45
|
-
pool,
|
|
89
|
+
pool: compatiblePool,
|
|
46
90
|
capability,
|
|
47
91
|
budget,
|
|
48
92
|
allowPaid,
|
|
@@ -51,31 +95,58 @@ export function buildFallbackChain(pool, capability, budget, allowPaid, options
|
|
|
51
95
|
...(options.paidCallsUsed !== undefined ? { paidCallsUsed: options.paidCallsUsed } : {}),
|
|
52
96
|
...(options.maxPaidCalls !== undefined ? { maxPaidCalls: options.maxPaidCalls } : {}),
|
|
53
97
|
});
|
|
54
|
-
|
|
55
|
-
|
|
98
|
+
// If every compatible candidate was budget-excluded (e.g. an all-paid pool
|
|
99
|
+
// under an eco budget or a fully exhausted paid cap), `resolveModel` has no
|
|
100
|
+
// winner and there is no valid primary — never fail open to the first raw
|
|
101
|
+
// pool entry, which could surface a paid model as primary.
|
|
102
|
+
if (!winner)
|
|
56
103
|
return undefined;
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
104
|
+
const primary = winner.id;
|
|
105
|
+
const paidCallsUsed = options.paidCallsUsed ?? 0;
|
|
106
|
+
const costOrder = costPreferenceOrder(budget, options.preferredCosts);
|
|
107
|
+
const byId = new Map(compatiblePool.map((candidate) => [candidate.id, candidate]));
|
|
108
|
+
const primaryCandidate = byId.get(primary);
|
|
109
|
+
const primaryEntry = primaryCandidate ? toEntry(primaryCandidate, capability) : undefined;
|
|
110
|
+
const alternatives = compatiblePool
|
|
60
111
|
.filter((candidate) => candidate.id !== primary)
|
|
61
|
-
.
|
|
62
|
-
.
|
|
112
|
+
.filter((candidate) => isBudgetEligible(candidate, allowPaid, paidCallsUsed, options.maxPaidCalls))
|
|
113
|
+
.sort((a, b) => {
|
|
114
|
+
const compat = compatibilityTier(b, capability) - compatibilityTier(a, capability);
|
|
115
|
+
if (compat !== 0)
|
|
116
|
+
return compat;
|
|
117
|
+
if (a.priority !== b.priority)
|
|
118
|
+
return b.priority - a.priority;
|
|
119
|
+
const tier = TIER_RANK[b.tier] - TIER_RANK[a.tier];
|
|
120
|
+
if (tier !== 0)
|
|
121
|
+
return tier;
|
|
122
|
+
const cost = costPreferenceRank(a.cost, costOrder) - costPreferenceRank(b.cost, costOrder);
|
|
123
|
+
if (cost !== 0)
|
|
124
|
+
return cost;
|
|
125
|
+
return compareCodepoints(a.id, b.id);
|
|
126
|
+
})
|
|
127
|
+
.map((candidate) => toEntry(candidate, capability));
|
|
63
128
|
return {
|
|
64
129
|
primary,
|
|
65
130
|
alternatives,
|
|
66
131
|
all: primaryEntry ? [primaryEntry, ...alternatives] : alternatives,
|
|
67
132
|
};
|
|
68
133
|
}
|
|
69
|
-
function
|
|
134
|
+
function compatibilityTier(candidate, capability) {
|
|
135
|
+
return isCapabilityCompatible(candidate, capability) ? 1 : 0;
|
|
136
|
+
}
|
|
137
|
+
function toEntry(candidate, capability) {
|
|
70
138
|
return {
|
|
71
|
-
id: candidate
|
|
72
|
-
cost: candidate
|
|
73
|
-
costRank: COST_RANK[candidate
|
|
139
|
+
id: candidate.id,
|
|
140
|
+
cost: candidate.cost,
|
|
141
|
+
costRank: COST_RANK[candidate.cost],
|
|
142
|
+
compatible: isCapabilityCompatible(candidate, capability),
|
|
143
|
+
priority: candidate.priority,
|
|
144
|
+
tier: candidate.tier,
|
|
74
145
|
};
|
|
75
146
|
}
|
|
76
147
|
/**
|
|
77
148
|
* Given a chain and the id of a model that just failed, return the next
|
|
78
|
-
*
|
|
149
|
+
* alternative to try, or undefined when the chain is exhausted.
|
|
79
150
|
*/
|
|
80
151
|
export function nextAfterFailure(chain, failedId) {
|
|
81
152
|
const index = chain.all.findIndex((entry) => entry.id === failedId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fallback.js","sourceRoot":"","sources":["../../src/routing/fallback.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"fallback.js","sourceRoot":"","sources":["../../src/routing/fallback.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import type { BudgetMode, CapabilityName, ModelCandidateInput, ModelCost, ModelTier } from "../config/schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Stable machine-readable routing decision identifier. The resolver only emits
|
|
4
|
+
* the scoring codes; `exact_override` is reserved for an explicit orch-lead
|
|
5
|
+
* agent override and is produced by the tool layer, not the resolver.
|
|
6
|
+
*/
|
|
7
|
+
export type RoutingReasonCode = "frontier" | "preferred_tier" | "preferred_cost" | "capability_match" | "price" | "priority" | "exact_override";
|
|
8
|
+
export interface RoutingReason {
|
|
9
|
+
code: RoutingReasonCode;
|
|
10
|
+
/** Bounded diagnostic copy; never contains secrets, prompts, or credentials. */
|
|
11
|
+
text: string;
|
|
12
|
+
matchedCapabilities: CapabilityName[];
|
|
13
|
+
score: number;
|
|
14
|
+
budget: BudgetMode;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Bound a routing reason's diagnostic text to a fixed length. The text only
|
|
18
|
+
* carries model id, cost class, tier, and score — never secrets, prompts, or
|
|
19
|
+
* credentials — so a plain truncation is safe.
|
|
20
|
+
*/
|
|
21
|
+
export declare function boundReasonText(text: string, limit?: number): string;
|
|
2
22
|
export interface ModelCandidate {
|
|
3
23
|
id: string;
|
|
4
24
|
cost: ModelCost;
|
|
@@ -23,6 +43,13 @@ export interface ResolveModelRequest {
|
|
|
23
43
|
paidCallsUsed?: number;
|
|
24
44
|
maxPaidCalls?: number;
|
|
25
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* The shared resolution parameters for the orch-lead agent. Both the tool
|
|
48
|
+
* layer (routing metadata) and `createLeadAgent` (the actually assigned model)
|
|
49
|
+
* resolve the lead pool through this exact request so the reported lead always
|
|
50
|
+
* matches the assigned one.
|
|
51
|
+
*/
|
|
52
|
+
export declare function leadResolveRequest(budget: BudgetMode): Omit<ResolveModelRequest, "pool" | "capability">;
|
|
26
53
|
export interface ResolvedModel {
|
|
27
54
|
id: string;
|
|
28
55
|
score: number;
|
|
@@ -35,6 +62,26 @@ export interface ResolvedModel {
|
|
|
35
62
|
* with a retryable error, step down this list instead of re-hitting it.
|
|
36
63
|
*/
|
|
37
64
|
fallback: string[];
|
|
65
|
+
/** Structured, machine-readable routing decision for the winning model. */
|
|
66
|
+
routingReason?: RoutingReason;
|
|
38
67
|
}
|
|
39
68
|
export declare function normalizeCandidate(input: ModelCandidateInput): ModelCandidate;
|
|
69
|
+
/**
|
|
70
|
+
* Shared capability scoring helpers. `resolveModel` uses these for scoring and
|
|
71
|
+
* `buildFallbackChain` uses them to filter and rank fallback alternatives.
|
|
72
|
+
*/
|
|
73
|
+
export declare function hasDeclaredCapability(candidate: ModelCandidate, capability: CapabilityName): boolean;
|
|
74
|
+
export declare function hasExplicitScore(candidate: ModelCandidate, capability: CapabilityName): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* A candidate is compatible when it either declares the capability or carries
|
|
77
|
+
* an explicit positive score for it.
|
|
78
|
+
*/
|
|
79
|
+
export declare function isCapabilityCompatible(candidate: ModelCandidate, capability: CapabilityName): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* A candidate is explicitly incompatible when it declares *some* capabilities,
|
|
82
|
+
* does not declare the required one, and has no explicit score for it. Unknown
|
|
83
|
+
* candidates (empty capabilities) are NOT incompatible: they stay eligible but
|
|
84
|
+
* rank at lower confidence.
|
|
85
|
+
*/
|
|
86
|
+
export declare function isCapabilityIncompatible(candidate: ModelCandidate, capability: CapabilityName): boolean;
|
|
40
87
|
export declare function resolveModel(request: ResolveModelRequest): ResolvedModel | undefined;
|
|
@@ -1,4 +1,38 @@
|
|
|
1
1
|
import { costAdjustment, getBudgetPolicy } from "./budget.js";
|
|
2
|
+
/** Maximum length of the `RoutingReason.text` diagnostic string. */
|
|
3
|
+
const REASON_TEXT_LIMIT = 200;
|
|
4
|
+
/**
|
|
5
|
+
* Bound a routing reason's diagnostic text to a fixed length. The text only
|
|
6
|
+
* carries model id, cost class, tier, and score — never secrets, prompts, or
|
|
7
|
+
* credentials — so a plain truncation is safe.
|
|
8
|
+
*/
|
|
9
|
+
export function boundReasonText(text, limit = REASON_TEXT_LIMIT) {
|
|
10
|
+
if (text.length <= limit)
|
|
11
|
+
return text;
|
|
12
|
+
return text.slice(0, limit - 1) + "…";
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The shared resolution parameters for the orch-lead agent. Both the tool
|
|
16
|
+
* layer (routing metadata) and `createLeadAgent` (the actually assigned model)
|
|
17
|
+
* resolve the lead pool through this exact request so the reported lead always
|
|
18
|
+
* matches the assigned one.
|
|
19
|
+
*/
|
|
20
|
+
export function leadResolveRequest(budget) {
|
|
21
|
+
return {
|
|
22
|
+
budget,
|
|
23
|
+
allowPaid: budget === "quality" || budget === "ebobo",
|
|
24
|
+
preferredCosts: budget === "balanced"
|
|
25
|
+
? ["subscription"]
|
|
26
|
+
: budget === "eco"
|
|
27
|
+
? ["free"]
|
|
28
|
+
: [],
|
|
29
|
+
preferredTiers: budget === "balanced"
|
|
30
|
+
? ["lead"]
|
|
31
|
+
: budget === "quality" || budget === "ebobo"
|
|
32
|
+
? ["frontier", "lead"]
|
|
33
|
+
: [],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
2
36
|
export function normalizeCandidate(input) {
|
|
3
37
|
if (typeof input === "string") {
|
|
4
38
|
return {
|
|
@@ -22,6 +56,34 @@ export function normalizeCandidate(input) {
|
|
|
22
56
|
...(input.priceOutput !== undefined ? { priceOutput: input.priceOutput } : {}),
|
|
23
57
|
};
|
|
24
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Shared capability scoring helpers. `resolveModel` uses these for scoring and
|
|
61
|
+
* `buildFallbackChain` uses them to filter and rank fallback alternatives.
|
|
62
|
+
*/
|
|
63
|
+
export function hasDeclaredCapability(candidate, capability) {
|
|
64
|
+
return candidate.capabilities.includes(capability);
|
|
65
|
+
}
|
|
66
|
+
export function hasExplicitScore(candidate, capability) {
|
|
67
|
+
return (candidate.scores[capability] ?? 0) > 0;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A candidate is compatible when it either declares the capability or carries
|
|
71
|
+
* an explicit positive score for it.
|
|
72
|
+
*/
|
|
73
|
+
export function isCapabilityCompatible(candidate, capability) {
|
|
74
|
+
return hasDeclaredCapability(candidate, capability) || hasExplicitScore(candidate, capability);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A candidate is explicitly incompatible when it declares *some* capabilities,
|
|
78
|
+
* does not declare the required one, and has no explicit score for it. Unknown
|
|
79
|
+
* candidates (empty capabilities) are NOT incompatible: they stay eligible but
|
|
80
|
+
* rank at lower confidence.
|
|
81
|
+
*/
|
|
82
|
+
export function isCapabilityIncompatible(candidate, capability) {
|
|
83
|
+
return candidate.capabilities.length > 0
|
|
84
|
+
&& !hasDeclaredCapability(candidate, capability)
|
|
85
|
+
&& !hasExplicitScore(candidate, capability);
|
|
86
|
+
}
|
|
25
87
|
const COST_ORDER = { paid: 2, subscription: 1, free: 0 };
|
|
26
88
|
function compareCodepoints(a, b) {
|
|
27
89
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -42,7 +104,7 @@ export function resolveModel(request) {
|
|
|
42
104
|
.filter((candidate) => candidate.cost !== "paid" || request.maxPaidCalls === undefined || (request.paidCallsUsed ?? 0) < request.maxPaidCalls)
|
|
43
105
|
.map((candidate) => {
|
|
44
106
|
const explicit = candidate.scores[request.capability] ?? 0;
|
|
45
|
-
const declared = candidate
|
|
107
|
+
const declared = hasDeclaredCapability(candidate, request.capability) ? 18 : 0;
|
|
46
108
|
const contextBonus = candidate.context === "xlarge" ? 8 : candidate.context === "large" ? 4 : 0;
|
|
47
109
|
const frontierBonus = request.budget === "ebobo"
|
|
48
110
|
? candidate.tier === "frontier"
|
|
@@ -67,7 +129,17 @@ export function resolveModel(request) {
|
|
|
67
129
|
...(preferredTierBonus ? ["preferred-tier=" + preferredTierBonus] : []),
|
|
68
130
|
...(priceAdj !== 0 ? ["price=" + priceAdj] : []),
|
|
69
131
|
];
|
|
70
|
-
return {
|
|
132
|
+
return {
|
|
133
|
+
candidate,
|
|
134
|
+
score,
|
|
135
|
+
reason,
|
|
136
|
+
explicit,
|
|
137
|
+
declared,
|
|
138
|
+
frontierBonus,
|
|
139
|
+
preferredCostBonus,
|
|
140
|
+
preferredTierBonus,
|
|
141
|
+
priceAdj,
|
|
142
|
+
};
|
|
71
143
|
})
|
|
72
144
|
.sort((a, b) => b.score - a.score || compareCodepoints(a.candidate.id, b.candidate.id));
|
|
73
145
|
const winner = ranked[0];
|
|
@@ -77,6 +149,41 @@ export function resolveModel(request) {
|
|
|
77
149
|
.slice(1)
|
|
78
150
|
.sort((a, b) => COST_ORDER[a.candidate.cost] - COST_ORDER[b.candidate.cost] || a.score - b.score || compareCodepoints(a.candidate.id, b.candidate.id))
|
|
79
151
|
.map((item) => item.candidate.id);
|
|
152
|
+
const matchedCapabilities = winner.explicit || winner.declared ? [request.capability] : [];
|
|
153
|
+
// Deterministic reason code by dominant scoring signal, strongest first.
|
|
154
|
+
let code;
|
|
155
|
+
let component;
|
|
156
|
+
if (winner.frontierBonus > 0) {
|
|
157
|
+
code = "frontier";
|
|
158
|
+
component = "frontier=" + winner.frontierBonus;
|
|
159
|
+
}
|
|
160
|
+
else if (winner.preferredTierBonus > 0) {
|
|
161
|
+
code = "preferred_tier";
|
|
162
|
+
component = "tier=" + winner.candidate.tier;
|
|
163
|
+
}
|
|
164
|
+
else if (winner.preferredCostBonus > 0) {
|
|
165
|
+
code = "preferred_cost";
|
|
166
|
+
component = "cost=" + winner.candidate.cost;
|
|
167
|
+
}
|
|
168
|
+
else if (winner.explicit || winner.declared) {
|
|
169
|
+
code = "capability_match";
|
|
170
|
+
component = "capability=" + (winner.explicit ? "explicit" : "declared");
|
|
171
|
+
}
|
|
172
|
+
else if (winner.priceAdj !== 0) {
|
|
173
|
+
code = "price";
|
|
174
|
+
component = "price=" + winner.priceAdj;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
code = "priority";
|
|
178
|
+
component = "priority=" + winner.candidate.priority;
|
|
179
|
+
}
|
|
180
|
+
const routingReason = {
|
|
181
|
+
code,
|
|
182
|
+
text: boundReasonText(`id=${winner.candidate.id} cost=${winner.candidate.cost} ${component} score=${winner.score}`),
|
|
183
|
+
matchedCapabilities,
|
|
184
|
+
score: winner.score,
|
|
185
|
+
budget: request.budget,
|
|
186
|
+
};
|
|
80
187
|
return {
|
|
81
188
|
id: winner.candidate.id,
|
|
82
189
|
score: winner.score,
|
|
@@ -85,6 +192,7 @@ export function resolveModel(request) {
|
|
|
85
192
|
...(winner.candidate.priceOutput !== undefined ? { priceOutput: winner.candidate.priceOutput } : {}),
|
|
86
193
|
reason: winner.reason,
|
|
87
194
|
fallback,
|
|
195
|
+
routingReason,
|
|
88
196
|
};
|
|
89
197
|
}
|
|
90
198
|
//# sourceMappingURL=model-resolver.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-resolver.js","sourceRoot":"","sources":["../../src/routing/model-resolver.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"model-resolver.js","sourceRoot":"","sources":["../../src/routing/model-resolver.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAyB7D,oEAAoE;AACpE,MAAM,iBAAiB,GAAG,GAAG,CAAA;AAE7B;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,QAAgB,iBAAiB;IAC7E,IAAI,IAAI,CAAC,MAAM,IAAI,KAAK;QAAE,OAAO,IAAI,CAAA;IACrC,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,GAAG,GAAG,CAAA;AACvC,CAAC;AA4BD;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAkB;IACnD,OAAO;QACL,MAAM;QACN,SAAS,EAAE,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,OAAO;QACrD,cAAc,EAAE,MAAM,KAAK,UAAU;YACnC,CAAC,CAAC,CAAC,cAAc,CAAC;YAClB,CAAC,CAAC,MAAM,KAAK,KAAK;gBAChB,CAAC,CAAC,CAAC,MAAM,CAAC;gBACV,CAAC,CAAC,EAAE;QACR,cAAc,EAAE,MAAM,KAAK,UAAU;YACnC,CAAC,CAAC,CAAC,MAAM,CAAC;YACV,CAAC,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,OAAO;gBAC1C,CAAC,CAAC,CAAC,UAAU,EAAE,MAAM,CAAC;gBACtB,CAAC,CAAC,EAAE;KACT,CAAA;AACH,CAAC;AAkBD,MAAM,UAAU,kBAAkB,CAAC,KAA0B;IAC3D,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,QAAQ;YACd,QAAQ,EAAE,EAAE;YACZ,YAAY,EAAE,EAAE;YAChB,MAAM,EAAE,EAAE;SACX,CAAA;IACH,CAAC;IAED,OAAO;QACL,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpD,GAAG,CAAC,KAAK,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3E,GAAG,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC/E,CAAA;AACH,CAAC;AAED;;;GAGG;AAEH,MAAM,UAAU,qBAAqB,CAAC,SAAyB,EAAE,UAA0B;IACzF,OAAO,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAA;AACpD,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,SAAyB,EAAE,UAA0B;IACpF,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;AAChD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CAAC,SAAyB,EAAE,UAA0B;IAC1F,OAAO,qBAAqB,CAAC,SAAS,EAAE,UAAU,CAAC,IAAI,gBAAgB,CAAC,SAAS,EAAE,UAAU,CAAC,CAAA;AAChG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,SAAyB,EAAE,UAA0B;IAC5F,OAAO,SAAS,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC;WACnC,CAAC,qBAAqB,CAAC,SAAS,EAAE,UAAU,CAAC;WAC7C,CAAC,gBAAgB,CAAC,SAAS,EAAE,UAAU,CAAC,CAAA;AAC/C,CAAC;AAED,MAAM,UAAU,GAA8B,EAAE,IAAI,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAA;AAEnF,SAAS,iBAAiB,CAAC,CAAS,EAAE,CAAS;IAC7C,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACnC,CAAC;AAED,SAAS,eAAe,CAAC,SAAyB;IAChD,4EAA4E;IAC5E,2EAA2E;IAC3E,MAAM,KAAK,GAAG,CAAC,SAAS,CAAC,UAAU,IAAI,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,WAAW,IAAI,CAAC,CAAC,CAAA;IACxE,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,CAAC,CAAA;IACxB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAA;AAC/C,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,OAA4B;IACvD,MAAM,MAAM,GAAG,eAAe,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;IAC9C,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI;SACxB,GAAG,CAAC,kBAAkB,CAAC;SACvB,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,OAAO,CAAC,SAAS,IAAI,SAAS,CAAC,IAAI,KAAK,MAAM,CAAC;SACrE,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,IAAI,CAAC,OAAO,CAAC,aAAa,IAAI,CAAC,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC;SAC7I,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE;QACjB,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;QAC1D,MAAM,QAAQ,GAAG,qBAAqB,CAAC,SAAS,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;QAC9E,MAAM,YAAY,GAAG,SAAS,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAC/F,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,KAAK,OAAO;YAC9C,CAAC,CAAC,SAAS,CAAC,IAAI,KAAK,UAAU;gBAC7B,CAAC,CAAC,GAAG;gBACL,CAAC,CAAC,SAAS,CAAC,IAAI,KAAK,MAAM;oBACzB,CAAC,CAAC,EAAE;oBACJ,CAAC,CAAC,CAAC;YACP,CAAC,CAAC,CAAC,CAAA;QACL,MAAM,kBAAkB,GAAG,OAAO,CAAC,cAAc,EAAE,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;QAChF,MAAM,kBAAkB,GAAG,OAAO,CAAC,cAAc,EAAE,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;QAChF,MAAM,kBAAkB,GAAG,kBAAkB,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,GAAG,kBAAkB,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACpG,MAAM,kBAAkB,GAAG,kBAAkB,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,GAAG,kBAAkB,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACpG,MAAM,QAAQ,GAAG,eAAe,CAAC,SAAS,CAAC,CAAA;QAC3C,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,GAAG,QAAQ,GAAG,CAAC,GAAG,QAAQ,GAAG,YAAY,GAAG,aAAa;cACrF,kBAAkB,GAAG,kBAAkB,GAAG,cAAc,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAA;QAC/F,MAAM,MAAM,GAAG;YACb,WAAW,GAAG,SAAS,CAAC,QAAQ;YAChC,aAAa,GAAG,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;YACrE,OAAO,GAAG,SAAS,CAAC,IAAI;YACxB,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,WAAW,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACvD,GAAG,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,iBAAiB,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACvE,GAAG,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,iBAAiB,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACvE,GAAG,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;SACjD,CAAA;QACD,OAAO;YACL,SAAS;YACT,KAAK;YACL,MAAM;YACN,QAAQ;YACR,QAAQ;YACR,aAAa;YACb,kBAAkB;YAClB,kBAAkB;YAClB,QAAQ;SACT,CAAA;IACH,CAAC,CAAC;SACD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI,iBAAiB,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAA;IAEzF,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;IACxB,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAA;IAE7B,MAAM,QAAQ,GAAG,MAAM;SACpB,KAAK,CAAC,CAAC,CAAC;SACR,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI,iBAAiB,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;SACrJ,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAA;IAEnC,MAAM,mBAAmB,GACvB,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAEhE,yEAAyE;IACzE,IAAI,IAAuB,CAAA;IAC3B,IAAI,SAAiB,CAAA;IACrB,IAAI,MAAM,CAAC,aAAa,GAAG,CAAC,EAAE,CAAC;QAC7B,IAAI,GAAG,UAAU,CAAA;QACjB,SAAS,GAAG,WAAW,GAAG,MAAM,CAAC,aAAa,CAAA;IAChD,CAAC;SAAM,IAAI,MAAM,CAAC,kBAAkB,GAAG,CAAC,EAAE,CAAC;QACzC,IAAI,GAAG,gBAAgB,CAAA;QACvB,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAA;IAC7C,CAAC;SAAM,IAAI,MAAM,CAAC,kBAAkB,GAAG,CAAC,EAAE,CAAC;QACzC,IAAI,GAAG,gBAAgB,CAAA;QACvB,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAA;IAC7C,CAAC;SAAM,IAAI,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QAC9C,IAAI,GAAG,kBAAkB,CAAA;QACzB,SAAS,GAAG,aAAa,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAA;IACzE,CAAC;SAAM,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;QACjC,IAAI,GAAG,OAAO,CAAA;QACd,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;IACxC,CAAC;SAAM,CAAC;QACN,IAAI,GAAG,UAAU,CAAA;QACjB,SAAS,GAAG,WAAW,GAAG,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAA;IACrD,CAAC;IAED,MAAM,aAAa,GAAkB;QACnC,IAAI;QACJ,IAAI,EAAE,eAAe,CAAC,MAAM,MAAM,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,CAAC,SAAS,CAAC,IAAI,IAAI,SAAS,UAAU,MAAM,CAAC,KAAK,EAAE,CAAC;QACnH,mBAAmB;QACnB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,MAAM,EAAE,OAAO,CAAC,MAAM;KACvB,CAAA;IAED,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,SAAS,CAAC,EAAE;QACvB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,IAAI,EAAE,MAAM,CAAC,SAAS,CAAC,IAAI;QAC3B,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,SAAS,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjG,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpG,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,QAAQ;QACR,aAAa;KACd,CAAA;AACH,CAAC"}
|
package/dist/spawn.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
export type SpawnSyncOptions = NonNullable<Parameters<typeof spawnSync>[2]>;
|
|
3
|
+
type SpawnSyncDependency = (command: string, args: string[], options: SpawnSyncOptions) => ReturnType<typeof spawnSync>;
|
|
4
|
+
export interface SpawnFallbackDependencies {
|
|
5
|
+
platform?: NodeJS.Platform;
|
|
6
|
+
spawnSync?: SpawnSyncDependency;
|
|
7
|
+
}
|
|
3
8
|
/**
|
|
4
9
|
* Quote a command line for cmd.exe: parts containing spaces or cmd
|
|
5
10
|
* metacharacters are wrapped in double quotes so paths with spaces and
|
|
@@ -16,10 +21,18 @@ export declare function safeForCmdRetry(parts: string[]): boolean;
|
|
|
16
21
|
* with EINVAL on CVE-2024-27980-patched Node). The retry is skipped for
|
|
17
22
|
* inputs cmd.exe could reinterpret — see {@link safeForCmdRetry} — and
|
|
18
23
|
* off-Windows the native result is always returned untouched.
|
|
24
|
+
*
|
|
25
|
+
* When a bare command name resolves to a `.cmd`/`.bat` shim ahead of any
|
|
26
|
+
* spawnable executable on Windows, the direct spawn is skipped entirely and
|
|
27
|
+
* the command runs through cmd.exe, mirroring cmd.exe's own resolution order.
|
|
28
|
+
* This keeps PATH-based shims authoritative even when a real `.exe` of the
|
|
29
|
+
* same name sits further down PATH (patched Node would otherwise bypass the
|
|
30
|
+
* shim and run the real tool).
|
|
19
31
|
*/
|
|
20
|
-
export declare function spawnWithCmdFallback(command: string, args: string[], options?: SpawnSyncOptions): ReturnType<typeof spawnSync>;
|
|
32
|
+
export declare function spawnWithCmdFallback(command: string, args: string[], options?: SpawnSyncOptions, deps?: SpawnFallbackDependencies): ReturnType<typeof spawnSync>;
|
|
21
33
|
/**
|
|
22
34
|
* User home for tool discovery. Windows tests override HOME to shadow the
|
|
23
35
|
* real per-user toolchain, so honor it before falling back to os.homedir().
|
|
24
36
|
*/
|
|
25
37
|
export declare function homeDirectory(): string;
|
|
38
|
+
export {};
|
package/dist/spawn.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { statSync } from "node:fs";
|
|
2
3
|
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
3
6
|
/**
|
|
4
7
|
* Quote a command line for cmd.exe: parts containing spaces or cmd
|
|
5
8
|
* metacharacters are wrapped in double quotes so paths with spaces and
|
|
@@ -26,17 +29,77 @@ const UNSAFE_FOR_CMD_RETRY = /["%\r\n;,!]/;
|
|
|
26
29
|
export function safeForCmdRetry(parts) {
|
|
27
30
|
return !parts.some((part) => UNSAFE_FOR_CMD_RETRY.test(part));
|
|
28
31
|
}
|
|
32
|
+
/** cmd.exe's default executable search order within a directory. */
|
|
33
|
+
const DEFAULT_PATHEXT = [".COM", ".EXE", ".BAT", ".CMD"];
|
|
34
|
+
/**
|
|
35
|
+
* Mirror cmd.exe's PATH resolution for a bare command name: scan the current
|
|
36
|
+
* directory and then PATH in order, trying PATHEXT extensions within each
|
|
37
|
+
* directory, and return the first existing file. Patched Windows Node skips
|
|
38
|
+
* `.cmd`/`.bat` candidates when resolving direct spawns, so a `.cmd` shim in
|
|
39
|
+
* an earlier PATH entry would otherwise be silently shadowed by a real `.exe`
|
|
40
|
+
* further down PATH. Knowing which candidate wins lets us route `.cmd`/`.bat`
|
|
41
|
+
* wins through the cmd.exe retry deterministically.
|
|
42
|
+
*/
|
|
43
|
+
function resolveCommandCandidate(command, env, cwd) {
|
|
44
|
+
const pathext = (env.PATHEXT ?? DEFAULT_PATHEXT.join(";"))
|
|
45
|
+
.split(";")
|
|
46
|
+
.map((extension) => extension.trim())
|
|
47
|
+
.filter((extension) => extension.length > 0)
|
|
48
|
+
.map((extension) => (extension.startsWith(".") ? extension : "." + extension));
|
|
49
|
+
const directories = [cwd, ...(env.PATH ?? "").split(";").map((directory) => directory.replace(/^"|"$/g, ""))].filter((directory) => directory.length > 0);
|
|
50
|
+
for (const directory of directories) {
|
|
51
|
+
for (const extension of pathext) {
|
|
52
|
+
const candidate = path.join(directory, command + extension);
|
|
53
|
+
try {
|
|
54
|
+
if (statSync(candidate).isFile())
|
|
55
|
+
return candidate;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Not a file; try the next extension.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
29
64
|
/**
|
|
30
65
|
* Run a command natively, retrying through cmd.exe when Windows cannot
|
|
31
66
|
* spawn the target directly (`.cmd`/`.bat` shims and shell scripts fail
|
|
32
67
|
* with EINVAL on CVE-2024-27980-patched Node). The retry is skipped for
|
|
33
68
|
* inputs cmd.exe could reinterpret — see {@link safeForCmdRetry} — and
|
|
34
69
|
* off-Windows the native result is always returned untouched.
|
|
70
|
+
*
|
|
71
|
+
* When a bare command name resolves to a `.cmd`/`.bat` shim ahead of any
|
|
72
|
+
* spawnable executable on Windows, the direct spawn is skipped entirely and
|
|
73
|
+
* the command runs through cmd.exe, mirroring cmd.exe's own resolution order.
|
|
74
|
+
* This keeps PATH-based shims authoritative even when a real `.exe` of the
|
|
75
|
+
* same name sits further down PATH (patched Node would otherwise bypass the
|
|
76
|
+
* shim and run the real tool).
|
|
35
77
|
*/
|
|
36
|
-
export function spawnWithCmdFallback(command, args, options = {}) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
78
|
+
export function spawnWithCmdFallback(command, args, options = {}, deps = {}) {
|
|
79
|
+
const runSpawn = deps.spawnSync || spawnSync;
|
|
80
|
+
const platform = deps.platform || process.platform;
|
|
81
|
+
const isBareName = !command.includes("\\") && !command.includes("/") && path.extname(command).length === 0;
|
|
82
|
+
let shellAttempt;
|
|
83
|
+
if (platform === "win32" && isBareName && safeForCmdRetry([command, ...args])) {
|
|
84
|
+
const env = options.env ?? process.env;
|
|
85
|
+
const cwd = typeof options.cwd === "string"
|
|
86
|
+
? options.cwd
|
|
87
|
+
: options.cwd instanceof URL
|
|
88
|
+
? fileURLToPath(options.cwd)
|
|
89
|
+
: process.cwd();
|
|
90
|
+
const candidate = resolveCommandCandidate(command, env, cwd);
|
|
91
|
+
if (candidate !== undefined && /\.(cmd|bat)$/i.test(candidate)) {
|
|
92
|
+
shellAttempt = runSpawn(quoteLineForCmd([command, ...args]), [], { ...options, shell: true });
|
|
93
|
+
if (shellAttempt.error === undefined)
|
|
94
|
+
return shellAttempt;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const direct = runSpawn(command, args, options);
|
|
98
|
+
// When the shim already ran through cmd.exe above and failed to SPAWN, do
|
|
99
|
+
// not shell out a second time — the shim could execute twice, doubling its
|
|
100
|
+
// side effects. Fail closed with the direct-spawn result instead.
|
|
101
|
+
if (platform === "win32" && direct.error !== undefined && shellAttempt === undefined && safeForCmdRetry([command, ...args])) {
|
|
102
|
+
const retry = runSpawn(quoteLineForCmd([command, ...args]), [], { ...options, shell: true });
|
|
40
103
|
if (retry.error === undefined)
|
|
41
104
|
return retry;
|
|
42
105
|
}
|