@juno-ai/bind 2.0.0 → 4.0.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 +1153 -60
- package/contracts/index.d.ts +1 -1
- package/contracts/index.js +1 -1
- package/contracts/turn.d.ts +31 -7
- package/contracts/turn.js +45 -0
- package/index.d.ts +16 -5
- package/index.js +16 -5
- package/loop/index.d.ts +1 -0
- package/loop/index.js +1 -0
- package/loop/tool-loop.d.ts +260 -0
- package/loop/tool-loop.js +276 -0
- package/package.json +22 -2
- package/plugins/activation.d.ts +67 -0
- package/plugins/activation.js +61 -0
- package/plugins/index.d.ts +3 -0
- package/plugins/index.js +3 -0
- package/plugins/registry.d.ts +52 -0
- package/plugins/registry.js +54 -0
- package/plugins/tool.d.ts +164 -0
- package/plugins/tool.js +9 -0
- package/routing/billing-basis.d.ts +48 -0
- package/routing/billing-basis.js +67 -0
- package/routing/circuit-breaker.d.ts +2 -2
- package/routing/errors.d.ts +1 -1
- package/routing/executor.d.ts +3 -3
- package/routing/executor.js +1 -1
- package/routing/index.d.ts +11 -9
- package/routing/index.js +11 -9
- package/routing/plan-degradation.d.ts +34 -0
- package/routing/plan-degradation.js +38 -0
- package/routing/plan.d.ts +2 -2
- package/routing/planner.d.ts +4 -4
- package/routing/planner.js +1 -1
- package/routing/policy.d.ts +1 -1
- package/routing/policy.js +1 -1
- package/routing/transport.d.ts +2 -2
- package/run/children.d.ts +204 -0
- package/run/children.js +226 -0
- package/run/harness.d.ts +94 -0
- package/run/harness.js +140 -0
- package/run/index.d.ts +3 -0
- package/run/index.js +3 -0
- package/run/tool-batch.d.ts +16 -0
- package/run/tool-batch.js +83 -0
- package/tools/index.d.ts +1 -0
- package/tools/index.js +1 -0
- package/tools/sanitize-schema.d.ts +150 -0
- package/tools/sanitize-schema.js +683 -0
- package/transcript/index.d.ts +1 -0
- package/transcript/index.js +1 -0
- package/transcript/validate.d.ts +54 -0
- package/transcript/validate.js +226 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* The plugin/tool vocabulary — the shape of a tool, a bundle of tools, and
|
|
4
|
+
* the result of running one.
|
|
5
|
+
*
|
|
6
|
+
* Two things are deliberately NOT fixed here, because they are where hosts
|
|
7
|
+
* genuinely diverge rather than incidentally differ:
|
|
8
|
+
*
|
|
9
|
+
* - **The invocation context** (`TCtx`). A census of two production agent
|
|
10
|
+
* runtimes' contexts found ~33 fields of which exactly one (`abortSignal`)
|
|
11
|
+
* was shared; the rest are host identity (tenant, workspace, thread, role),
|
|
12
|
+
* host authorization (principal, scope clamps), or host product
|
|
13
|
+
* features. A common concrete context would be either a
|
|
14
|
+
* lowest-common-denominator or a union of two products' identity models,
|
|
15
|
+
* so the context is a type parameter and the host supplies it.
|
|
16
|
+
*
|
|
17
|
+
* - **Multimodal content parts** (`TContentPart`). The wire shape is the
|
|
18
|
+
* provider's, but building one from bytes needs host capabilities, so the
|
|
19
|
+
* harness carries the parts through without interpreting them.
|
|
20
|
+
*
|
|
21
|
+
* Hosts extend `ToolPlugin` with their own fields via ordinary interface
|
|
22
|
+
* extension (Monad adds `configuration`, `skills`, `exposeViaMcp`).
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Behavioural hints that travel with a tool on `tools/list`. Mirrors
|
|
26
|
+
* the MCP `ToolAnnotations` shape — duplicated here so plugins can
|
|
27
|
+
* declare them without importing the SDK directly.
|
|
28
|
+
*/
|
|
29
|
+
export interface ToolAnnotations {
|
|
30
|
+
/** Human-readable title for the tool. */
|
|
31
|
+
title?: string;
|
|
32
|
+
/** True if the tool only reads data. */
|
|
33
|
+
readOnlyHint?: boolean;
|
|
34
|
+
/** True if the tool may make destructive changes. Meaningful only when readOnlyHint===false. */
|
|
35
|
+
destructiveHint?: boolean;
|
|
36
|
+
/** True if calling the tool with the same args yields the same result. */
|
|
37
|
+
idempotentHint?: boolean;
|
|
38
|
+
/** True if the tool may interact with external systems. */
|
|
39
|
+
openWorldHint?: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface ToolDef {
|
|
42
|
+
name: string;
|
|
43
|
+
description: string;
|
|
44
|
+
parameters: z.ZodType;
|
|
45
|
+
/**
|
|
46
|
+
* Pure post-parse canonicalization applied before run-scoped idempotency
|
|
47
|
+
* hashing and execution. Keep model-facing schemas JSON-Schema-compatible;
|
|
48
|
+
* semantic normalization such as sorting set-like arrays belongs here.
|
|
49
|
+
*/
|
|
50
|
+
normalizeArgs?: (args: unknown) => unknown;
|
|
51
|
+
/**
|
|
52
|
+
* Pre-computed JSON Schema to pass to the LLM instead of converting via
|
|
53
|
+
* `z.toJSONSchema()`. Also the seam for a host whose tools are authored as
|
|
54
|
+
* raw JSON Schema rather than zod.
|
|
55
|
+
*/
|
|
56
|
+
rawJsonSchema?: Record<string, unknown>;
|
|
57
|
+
/** Annotations forwarded on the MCP `tools/list` response. */
|
|
58
|
+
annotations?: ToolAnnotations;
|
|
59
|
+
/** If true, the dispatcher emits a periodic auto-heartbeat. */
|
|
60
|
+
supportsProgress?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* When `true`, the tool stays runnable (dispatch + `plugin.tools` lookup
|
|
63
|
+
* still resolve it) but is omitted from the model-facing catalog. Used to
|
|
64
|
+
* retire a tool from the prompt while keeping it callable for resumed
|
|
65
|
+
* sessions whose history still references it.
|
|
66
|
+
*/
|
|
67
|
+
hidden?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Optional human one-liner describing THIS call, derived from its args.
|
|
70
|
+
* MUST be pure + synchronous: `safeParse` the args with the tool's own
|
|
71
|
+
* schema, then read fields — no I/O, no context. Returns `null` to fall
|
|
72
|
+
* back to generic copy. Purity is load-bearing: it runs on the executor
|
|
73
|
+
* hot path inside a fire-and-forget emit.
|
|
74
|
+
*/
|
|
75
|
+
summarizeActivity?: (args: unknown) => string | null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Structured failure category for a failed `ToolResult`. Lets a host map a
|
|
79
|
+
* plugin failure onto its own error taxonomy — a protocol error code, an HTTP
|
|
80
|
+
* status, a retry decision — without matching on the error string, which is
|
|
81
|
+
* brittle and locale-dependent. Plugins should set it explicitly.
|
|
82
|
+
*/
|
|
83
|
+
export type ToolFailureKind = "authz" | "validation" | "not_found" | "conflict" | "external" | "system";
|
|
84
|
+
/**
|
|
85
|
+
* Human-in-the-loop suspend directive. A first-party tool returns this on a
|
|
86
|
+
* successful result to **end the run** and record its open tool-call as
|
|
87
|
+
* awaiting resolution. Two resume kinds:
|
|
88
|
+
* - `answer`: the run resumes by threading a human/external response back as
|
|
89
|
+
* the matching `role:"tool"` result. The loop therefore WITHHOLDS this
|
|
90
|
+
* call's tool message — the result is the future answer.
|
|
91
|
+
* - `wake`: a time/event resume that re-enters via a prompt and keeps the
|
|
92
|
+
* tool message.
|
|
93
|
+
* `request` is an opaque render/route payload, validated by the consumer and
|
|
94
|
+
* never inspected by the loop.
|
|
95
|
+
*/
|
|
96
|
+
export type SuspendDirective = {
|
|
97
|
+
reason: string;
|
|
98
|
+
resumeKind: "answer" | "wake";
|
|
99
|
+
request?: unknown;
|
|
100
|
+
};
|
|
101
|
+
export type ToolResult<TContentPart = unknown> = {
|
|
102
|
+
success: true;
|
|
103
|
+
data: unknown;
|
|
104
|
+
contentParts?: TContentPart[];
|
|
105
|
+
suspend?: SuspendDirective;
|
|
106
|
+
} | {
|
|
107
|
+
success: false;
|
|
108
|
+
error: string;
|
|
109
|
+
kind?: ToolFailureKind;
|
|
110
|
+
/** Optional structured recovery context safe to expose to the model. */
|
|
111
|
+
data?: unknown;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Narrow helper: does this tool result ask the executor to relay multimodal
|
|
115
|
+
* content parts alongside the textual JSON result on the next turn?
|
|
116
|
+
*/
|
|
117
|
+
export declare function hasContentParts<TContentPart>(result: ToolResult<TContentPart>): result is {
|
|
118
|
+
success: true;
|
|
119
|
+
data: unknown;
|
|
120
|
+
contentParts: TContentPart[];
|
|
121
|
+
suspend?: SuspendDirective;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* A self-contained bundle of related tools plus the metadata the catalog and
|
|
125
|
+
* the system prompt need. `execute` dispatches by tool name so a plugin can
|
|
126
|
+
* share setup across its tools.
|
|
127
|
+
*/
|
|
128
|
+
export interface ToolPlugin<TCtx, TContentPart = unknown> {
|
|
129
|
+
name: string;
|
|
130
|
+
description: string;
|
|
131
|
+
/**
|
|
132
|
+
* Optional short icon identifier a host UI may use to pick a glyph. Optional
|
|
133
|
+
* because a headless consumer has no glyph to pick; a host that renders tool
|
|
134
|
+
* activity can re-require it on its own extension of this interface.
|
|
135
|
+
*/
|
|
136
|
+
icon?: string;
|
|
137
|
+
/**
|
|
138
|
+
* Global availability gate; unavailable plugins stay registered (so they can
|
|
139
|
+
* still be listed for configuration) but do not resolve for a run.
|
|
140
|
+
*
|
|
141
|
+
* **Must be constant for the lifetime of the process or isolate.** It is
|
|
142
|
+
* re-evaluated on every catalog render — which, for a host that re-renders
|
|
143
|
+
* its system prompt on each activation, is several times per run. A value
|
|
144
|
+
* that can flip mid-run (a TTL-cached feature flag, a health probe, a live
|
|
145
|
+
* credential check) rewrites the catalog and invalidates the provider
|
|
146
|
+
* prompt-cache prefix from that byte onward for every remaining turn, and
|
|
147
|
+
* nothing will fail a test. Gate on process-stable configuration; do the
|
|
148
|
+
* liveness check inside `execute`, where a failure is a tool error.
|
|
149
|
+
*/
|
|
150
|
+
isAvailable?: () => boolean;
|
|
151
|
+
/** Instructions appended to the system prompt when this plugin is active. */
|
|
152
|
+
systemMessage?: string;
|
|
153
|
+
tools: ToolDef[];
|
|
154
|
+
execute(toolName: string, args: unknown, ctx: TCtx): Promise<ToolResult<TContentPart>>;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The structural minimum the registry and catalog need. Hosts pass their own
|
|
158
|
+
* richer plugin type; this is the constraint, not the contract.
|
|
159
|
+
*/
|
|
160
|
+
export interface RegistrablePlugin {
|
|
161
|
+
name: string;
|
|
162
|
+
description: string;
|
|
163
|
+
isAvailable?: () => boolean;
|
|
164
|
+
}
|
package/plugins/tool.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow helper: does this tool result ask the executor to relay multimodal
|
|
3
|
+
* content parts alongside the textual JSON result on the next turn?
|
|
4
|
+
*/
|
|
5
|
+
export function hasContentParts(result) {
|
|
6
|
+
return (result.success === true &&
|
|
7
|
+
Array.isArray(result.contentParts) &&
|
|
8
|
+
result.contentParts.length > 0);
|
|
9
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ProviderPricingBasis } from "./plan.js";
|
|
2
|
+
/**
|
|
3
|
+
* Synchronous provider billing-basis cost —
|
|
4
|
+
* the configured-list-rate calculation used when a provider (Azure) reports
|
|
5
|
+
* usage but not dollars. OpenRouter's provider-reported cost keeps the
|
|
6
|
+
* existing path in `llm.ts` and never goes through this function.
|
|
7
|
+
*
|
|
8
|
+
* Results are USD cents at the inference log's numeric(10, 4) precision.
|
|
9
|
+
*/
|
|
10
|
+
export interface BillingBasisUsage {
|
|
11
|
+
readonly inputTokens: number;
|
|
12
|
+
/**
|
|
13
|
+
* Provider-reported cached (discounted) input tokens. Whether this is a
|
|
14
|
+
* subset of `inputTokens` or a disjoint count is DECLARED by the binding's
|
|
15
|
+
* `cachedTokenSemantics` — never inferred from the relative sizes.
|
|
16
|
+
*/
|
|
17
|
+
readonly cachedInputTokens: number;
|
|
18
|
+
/**
|
|
19
|
+
* Provider completion-token total. Already includes reasoning tokens — do
|
|
20
|
+
* not add them a second time.
|
|
21
|
+
*/
|
|
22
|
+
readonly outputTokens: number;
|
|
23
|
+
}
|
|
24
|
+
export type BillingBasisResult = Readonly<{
|
|
25
|
+
ok: true;
|
|
26
|
+
costCents: number;
|
|
27
|
+
}> | Readonly<{
|
|
28
|
+
ok: false;
|
|
29
|
+
reason: string;
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* Compute the list-rate cost in USD cents from usage and configured
|
|
33
|
+
* per-million-token rates. Rejects internally inconsistent usage rather than
|
|
34
|
+
* producing a zero-cost call: negative counts, non-integers,
|
|
35
|
+
* all-zero usage (a request always consumes prompt tokens — zeros mean the
|
|
36
|
+
* provider's accounting is broken, not that the call was free), and — under
|
|
37
|
+
* declared `"subset"` semantics — cached counts exceeding total input.
|
|
38
|
+
*
|
|
39
|
+
* The uncached-input calculation follows the binding's declared
|
|
40
|
+
* `cachedTokenSemantics`:
|
|
41
|
+
* - `"subset"` — cached ⊆ input (OpenAI): uncached = input − cached.
|
|
42
|
+
* - `"disjoint"` — cached reported alongside input (Grok via Azure AI
|
|
43
|
+
* Foundry, observed live: cached 192 vs prompt 88): uncached = input.
|
|
44
|
+
* Either way every token prices exactly once at its own rate.
|
|
45
|
+
*/
|
|
46
|
+
export declare function computeConfiguredRatesCostCents(pricing: Extract<ProviderPricingBasis, {
|
|
47
|
+
kind: "configured_token_rates";
|
|
48
|
+
}>, usage: BillingBasisUsage): BillingBasisResult;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function isUsableCount(value) {
|
|
2
|
+
return Number.isFinite(value) && Number.isInteger(value) && value >= 0;
|
|
3
|
+
}
|
|
4
|
+
function roundToScale4(value) {
|
|
5
|
+
return Math.round(value * 10_000) / 10_000;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Compute the list-rate cost in USD cents from usage and configured
|
|
9
|
+
* per-million-token rates. Rejects internally inconsistent usage rather than
|
|
10
|
+
* producing a zero-cost call: negative counts, non-integers,
|
|
11
|
+
* all-zero usage (a request always consumes prompt tokens — zeros mean the
|
|
12
|
+
* provider's accounting is broken, not that the call was free), and — under
|
|
13
|
+
* declared `"subset"` semantics — cached counts exceeding total input.
|
|
14
|
+
*
|
|
15
|
+
* The uncached-input calculation follows the binding's declared
|
|
16
|
+
* `cachedTokenSemantics`:
|
|
17
|
+
* - `"subset"` — cached ⊆ input (OpenAI): uncached = input − cached.
|
|
18
|
+
* - `"disjoint"` — cached reported alongside input (Grok via Azure AI
|
|
19
|
+
* Foundry, observed live: cached 192 vs prompt 88): uncached = input.
|
|
20
|
+
* Either way every token prices exactly once at its own rate.
|
|
21
|
+
*/
|
|
22
|
+
export function computeConfiguredRatesCostCents(pricing, usage) {
|
|
23
|
+
if (!isUsableCount(usage.inputTokens)) {
|
|
24
|
+
return { ok: false, reason: `invalid input token count: ${usage.inputTokens}` };
|
|
25
|
+
}
|
|
26
|
+
if (!isUsableCount(usage.cachedInputTokens)) {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
reason: `invalid cached input token count: ${usage.cachedInputTokens}`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (!isUsableCount(usage.outputTokens)) {
|
|
33
|
+
return { ok: false, reason: `invalid output token count: ${usage.outputTokens}` };
|
|
34
|
+
}
|
|
35
|
+
if (usage.inputTokens === 0 &&
|
|
36
|
+
usage.cachedInputTokens === 0 &&
|
|
37
|
+
usage.outputTokens === 0) {
|
|
38
|
+
return { ok: false, reason: "all-zero usage (provider reported no tokens)" };
|
|
39
|
+
}
|
|
40
|
+
let uncachedInputTokens;
|
|
41
|
+
switch (pricing.cachedTokenSemantics) {
|
|
42
|
+
case "subset":
|
|
43
|
+
if (usage.cachedInputTokens > usage.inputTokens) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
reason: `cached input tokens (${usage.cachedInputTokens}) exceed input tokens ` +
|
|
47
|
+
`(${usage.inputTokens}) under declared "subset" semantics — if this ` +
|
|
48
|
+
`provider reports cached tokens alongside prompt tokens, declare ` +
|
|
49
|
+
`cachedTokenSemantics: "disjoint" on its binding`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
uncachedInputTokens = usage.inputTokens - usage.cachedInputTokens;
|
|
53
|
+
break;
|
|
54
|
+
case "disjoint":
|
|
55
|
+
uncachedInputTokens = usage.inputTokens;
|
|
56
|
+
break;
|
|
57
|
+
default: {
|
|
58
|
+
const _exhaustive = pricing.cachedTokenSemantics;
|
|
59
|
+
throw new Error(`unknown cached-token semantics: ${JSON.stringify(_exhaustive)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const usd = (uncachedInputTokens * pricing.inputUsdPerM +
|
|
63
|
+
usage.cachedInputTokens * pricing.cachedInputUsdPerM +
|
|
64
|
+
usage.outputTokens * pricing.outputUsdPerM) /
|
|
65
|
+
1_000_000;
|
|
66
|
+
return { ok: true, costCents: roundToScale4(usd * 100) };
|
|
67
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ProviderId } from "./canonical-model";
|
|
2
|
-
import type { CredentialSource } from "./plan";
|
|
1
|
+
import type { ProviderId } from "./canonical-model.js";
|
|
2
|
+
import type { CredentialSource } from "./plan.js";
|
|
3
3
|
/**
|
|
4
4
|
* Circuit-breaker state is keyed by (provider, invocation model, credential
|
|
5
5
|
* source) — LLM Provider Routing PRD §7.2. State is process-local in v1;
|
package/routing/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CanonicalModelId, ProviderId } from "./canonical-model";
|
|
1
|
+
import type { CanonicalModelId, ProviderId } from "./canonical-model.js";
|
|
2
2
|
/**
|
|
3
3
|
* The normative attempt-order cursor (LLM Provider Routing PRD §5.2):
|
|
4
4
|
* structured-output attempt → model stage → provider candidate → same-endpoint
|
package/routing/executor.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { RouteCandidate, RoutePlan } from "./plan";
|
|
2
|
-
import { type InferenceAttemptError, type RouteAttemptCursor } from "./errors";
|
|
3
|
-
import type { RouteCircuitBreaker } from "./circuit-breaker";
|
|
1
|
+
import type { RouteCandidate, RoutePlan } from "./plan.js";
|
|
2
|
+
import { type InferenceAttemptError, type RouteAttemptCursor } from "./errors.js";
|
|
3
|
+
import type { RouteCircuitBreaker } from "./circuit-breaker.js";
|
|
4
4
|
export type AttemptOutcome<T> = Readonly<{
|
|
5
5
|
kind: "success";
|
|
6
6
|
value: T;
|
package/routing/executor.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { failureDisposition, isRetriableAttemptError, } from "./errors";
|
|
1
|
+
import { failureDisposition, isRetriableAttemptError, } from "./errors.js";
|
|
2
2
|
/**
|
|
3
3
|
* Drive one structured-output attempt over a frozen route plan with the
|
|
4
4
|
* normative loop nesting (PRD §5.2): model stage → provider candidate →
|
package/routing/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
export { canonicalModelIdSchema, providerIdSchema, type CanonicalModelId, type ProviderId, } from "./canonical-model";
|
|
2
|
-
export { ROUTE_POLICY_VERSION, providerPolicySchema, modelRouteRegistrySchema, parseModelRouteRegistry, resolveProviderPolicy, policyProviderOrder, type RoutePolicyVersion, type ProviderPolicy, type ModelRouteRegistry, } from "./policy";
|
|
3
|
-
export { INFERENCE_CAPABILITIES, planIsEmpty, type InferenceCapability, type CredentialSource, type InferenceRequirements, type CachedTokenSemantics, type ProviderPricingBasis, type RouteCandidate, type RouteStage, type RoutePlan, type RouteSkip, } from "./plan";
|
|
4
|
-
export { type TransportAvailability, type CandidateResolution, type PlannerTransport, } from "./transport";
|
|
5
|
-
export { buildRoutePlan, type RoutePlanRequest, type RoutePlanResult } from "./planner";
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
1
|
+
export { canonicalModelIdSchema, providerIdSchema, type CanonicalModelId, type ProviderId, } from "./canonical-model.js";
|
|
2
|
+
export { ROUTE_POLICY_VERSION, providerPolicySchema, modelRouteRegistrySchema, parseModelRouteRegistry, resolveProviderPolicy, policyProviderOrder, type RoutePolicyVersion, type ProviderPolicy, type ModelRouteRegistry, } from "./policy.js";
|
|
3
|
+
export { INFERENCE_CAPABILITIES, planIsEmpty, type InferenceCapability, type CredentialSource, type InferenceRequirements, type CachedTokenSemantics, type ProviderPricingBasis, type RouteCandidate, type RouteStage, type RoutePlan, type RouteSkip, } from "./plan.js";
|
|
4
|
+
export { type TransportAvailability, type CandidateResolution, type PlannerTransport, } from "./transport.js";
|
|
5
|
+
export { buildRoutePlan, type RoutePlanRequest, type RoutePlanResult } from "./planner.js";
|
|
6
|
+
export { buildRoutePlanWithConfigDegradation, type DegradedStage, type DegradedPlanResult, } from "./plan-degradation.js";
|
|
7
|
+
export { computeConfiguredRatesCostCents, type BillingBasisUsage, type BillingBasisResult, } from "./billing-basis.js";
|
|
8
|
+
export { fallbackKindOfCursor } from "./executor.js";
|
|
9
|
+
export { failureDisposition, categorizeHttpStatus, isRetriableAttemptError, type RouteAttemptCursor, type AttemptTarget, type HttpFailureCategory, type InferenceAttemptError, type FailureDisposition, type BreakerEffect, } from "./errors.js";
|
|
10
|
+
export { createCircuitBreaker, type BreakerKey, type CircuitBreakerOptions, type EndpointAdmission, type RecordFailureOptions, type RouteCircuitBreaker, } from "./circuit-breaker.js";
|
|
11
|
+
export { executeRoutePlan, type AttemptOutcome, type AttemptFn, type FallbackKind, type ExecutePlanOptions, type RouteExecutionResult, } from "./executor.js";
|
package/routing/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
export { canonicalModelIdSchema, providerIdSchema, } from "./canonical-model";
|
|
2
|
-
export { ROUTE_POLICY_VERSION, providerPolicySchema, modelRouteRegistrySchema, parseModelRouteRegistry, resolveProviderPolicy, policyProviderOrder, } from "./policy";
|
|
3
|
-
export { INFERENCE_CAPABILITIES, planIsEmpty, } from "./plan";
|
|
4
|
-
export {} from "./transport";
|
|
5
|
-
export { buildRoutePlan } from "./planner";
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
1
|
+
export { canonicalModelIdSchema, providerIdSchema, } from "./canonical-model.js";
|
|
2
|
+
export { ROUTE_POLICY_VERSION, providerPolicySchema, modelRouteRegistrySchema, parseModelRouteRegistry, resolveProviderPolicy, policyProviderOrder, } from "./policy.js";
|
|
3
|
+
export { INFERENCE_CAPABILITIES, planIsEmpty, } from "./plan.js";
|
|
4
|
+
export {} from "./transport.js";
|
|
5
|
+
export { buildRoutePlan } from "./planner.js";
|
|
6
|
+
export { buildRoutePlanWithConfigDegradation, } from "./plan-degradation.js";
|
|
7
|
+
export { computeConfiguredRatesCostCents, } from "./billing-basis.js";
|
|
8
|
+
export { fallbackKindOfCursor } from "./executor.js";
|
|
9
|
+
export { failureDisposition, categorizeHttpStatus, isRetriableAttemptError, } from "./errors.js";
|
|
10
|
+
export { createCircuitBreaker, } from "./circuit-breaker.js";
|
|
11
|
+
export { executeRoutePlan, } from "./executor.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CanonicalModelId } from "./canonical-model.js";
|
|
2
|
+
import type { RouteSkip } from "./plan.js";
|
|
3
|
+
import { type RoutePlanRequest, type RoutePlanResult } from "./planner.js";
|
|
4
|
+
import { type ProviderPolicy } from "./policy.js";
|
|
5
|
+
/**
|
|
6
|
+
* Configuration-degradation wrapper around the harness planner.
|
|
7
|
+
*
|
|
8
|
+
* Policy: even when a provider has priority over the default (an explicit
|
|
9
|
+
* `only` fence or an ordered policy naming it first), **missing keys or
|
|
10
|
+
* configuration must never make inference unavailable** — the call degrades
|
|
11
|
+
* to the default provider and the caller logs an error. This keeps
|
|
12
|
+
* development environments that hold only an OpenRouter key fully working
|
|
13
|
+
* while a checked-in policy prioritizes (say) Azure, and keeps production
|
|
14
|
+
* serving through a provider-config regression instead of hard-failing.
|
|
15
|
+
*
|
|
16
|
+
* Scope: PLAN-TIME emptiness only — a stage with zero attemptable candidates
|
|
17
|
+
* (transport unconfigured/invalid, no binding for the model, capability
|
|
18
|
+
* mismatch). Runtime failures keep the fence semantics: a configured `only`
|
|
19
|
+
* provider that errors at request time is never retried through another
|
|
20
|
+
* provider; the executor's cross-provider traversal applies only
|
|
21
|
+
* within the plan the policy allowed.
|
|
22
|
+
*/
|
|
23
|
+
export interface DegradedStage {
|
|
24
|
+
readonly model: CanonicalModelId;
|
|
25
|
+
/** The policy that produced an empty stage. */
|
|
26
|
+
readonly fromPolicy: ProviderPolicy;
|
|
27
|
+
/** Plan skips recorded for this model under the original policy. */
|
|
28
|
+
readonly skips: readonly RouteSkip[];
|
|
29
|
+
}
|
|
30
|
+
export interface DegradedPlanResult extends RoutePlanResult {
|
|
31
|
+
/** Stages rebuilt under the degraded default; empty when nothing degraded. */
|
|
32
|
+
readonly degraded: readonly DegradedStage[];
|
|
33
|
+
}
|
|
34
|
+
export declare function buildRoutePlanWithConfigDegradation(request: RoutePlanRequest, degradedPolicy: ProviderPolicy): DegradedPlanResult;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { buildRoutePlan, } from "./planner.js";
|
|
2
|
+
import { policyProviderOrder } from "./policy.js";
|
|
3
|
+
function policiesEqual(a, b) {
|
|
4
|
+
const aOrder = policyProviderOrder(a);
|
|
5
|
+
const bOrder = policyProviderOrder(b);
|
|
6
|
+
return (a.mode === b.mode &&
|
|
7
|
+
aOrder.length === bOrder.length &&
|
|
8
|
+
aOrder.every((provider, index) => provider === bOrder[index]));
|
|
9
|
+
}
|
|
10
|
+
export function buildRoutePlanWithConfigDegradation(request, degradedPolicy) {
|
|
11
|
+
const first = buildRoutePlan(request);
|
|
12
|
+
const emptyStagePolicies = new Map();
|
|
13
|
+
for (const stage of first.plan.stages) {
|
|
14
|
+
if (stage.candidates.length === 0 &&
|
|
15
|
+
!policiesEqual(stage.policy, degradedPolicy)) {
|
|
16
|
+
emptyStagePolicies.set(stage.canonicalModelId, stage.policy);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (emptyStagePolicies.size === 0) {
|
|
20
|
+
return { plan: first.plan, skips: first.skips, degraded: [] };
|
|
21
|
+
}
|
|
22
|
+
// Deterministic rebuild: identical inputs except the empty stages' models
|
|
23
|
+
// now resolve to the degraded default policy. If the default provider is
|
|
24
|
+
// ALSO unavailable (e.g. no OpenRouter key at all) the rebuilt stage stays
|
|
25
|
+
// empty and the caller's no-viable-endpoint handling applies unchanged.
|
|
26
|
+
const second = buildRoutePlan({
|
|
27
|
+
...request,
|
|
28
|
+
policyFor: (model) => emptyStagePolicies.get(model) !== undefined
|
|
29
|
+
? degradedPolicy
|
|
30
|
+
: request.policyFor(model),
|
|
31
|
+
});
|
|
32
|
+
const degraded = [...emptyStagePolicies.entries()].map(([model, fromPolicy]) => ({
|
|
33
|
+
model,
|
|
34
|
+
fromPolicy,
|
|
35
|
+
skips: first.skips.filter((skip) => skip.canonicalModelId === model),
|
|
36
|
+
}));
|
|
37
|
+
return { plan: second.plan, skips: second.skips, degraded };
|
|
38
|
+
}
|
package/routing/plan.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { CanonicalModelId, ProviderId } from "./canonical-model";
|
|
2
|
-
import type { ProviderPolicy, RoutePolicyVersion } from "./policy";
|
|
1
|
+
import type { CanonicalModelId, ProviderId } from "./canonical-model.js";
|
|
2
|
+
import type { ProviderPolicy, RoutePolicyVersion } from "./policy.js";
|
|
3
3
|
/**
|
|
4
4
|
* Capabilities the planner checks before any network request (LLM Provider
|
|
5
5
|
* Routing PRD §5.1/§5.3). `max_completion_tokens` is modeled as a capability
|
package/routing/planner.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { CanonicalModelId, ProviderId } from "./canonical-model";
|
|
2
|
-
import { type ProviderPolicy } from "./policy";
|
|
3
|
-
import type { InferenceRequirements, RoutePlan, RouteSkip } from "./plan";
|
|
4
|
-
import type { PlannerTransport } from "./transport";
|
|
1
|
+
import type { CanonicalModelId, ProviderId } from "./canonical-model.js";
|
|
2
|
+
import { type ProviderPolicy } from "./policy.js";
|
|
3
|
+
import type { InferenceRequirements, RoutePlan, RouteSkip } from "./plan.js";
|
|
4
|
+
import type { PlannerTransport } from "./transport.js";
|
|
5
5
|
export interface RoutePlanRequest {
|
|
6
6
|
readonly primaryModel: CanonicalModelId;
|
|
7
7
|
/** A distinct fallback canonical model, or null. Equal to primary is ignored. */
|
package/routing/planner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ROUTE_POLICY_VERSION, policyProviderOrder, } from "./policy";
|
|
1
|
+
import { ROUTE_POLICY_VERSION, policyProviderOrder, } from "./policy.js";
|
|
2
2
|
/**
|
|
3
3
|
* Deterministically expand a canonical primary/fallback selection into an
|
|
4
4
|
* immutable, ordered route plan (LLM Provider Routing PRD §5.1). The only
|
package/routing/policy.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { type CanonicalModelId, type ProviderId } from "./canonical-model";
|
|
2
|
+
import { type CanonicalModelId, type ProviderId } from "./canonical-model.js";
|
|
3
3
|
/**
|
|
4
4
|
* Policy order and semantics are versioned; the version string travels into
|
|
5
5
|
* telemetry (LLM Provider Routing PRD §4.2) so a routing-behavior change is
|
package/routing/policy.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { canonicalModelIdSchema, providerIdSchema, } from "./canonical-model";
|
|
2
|
+
import { canonicalModelIdSchema, providerIdSchema, } from "./canonical-model.js";
|
|
3
3
|
/**
|
|
4
4
|
* Policy order and semantics are versioned; the version string travels into
|
|
5
5
|
* telemetry (LLM Provider Routing PRD §4.2) so a routing-behavior change is
|
package/routing/transport.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { CanonicalModelId, ProviderId } from "./canonical-model";
|
|
2
|
-
import type { InferenceRequirements, RouteCandidate, RouteSkip } from "./plan";
|
|
1
|
+
import type { CanonicalModelId, ProviderId } from "./canonical-model.js";
|
|
2
|
+
import type { InferenceRequirements, RouteCandidate, RouteSkip } from "./plan.js";
|
|
3
3
|
export type TransportAvailability = Readonly<{
|
|
4
4
|
available: true;
|
|
5
5
|
}> | Readonly<{
|