@oscharko-dev/keiko-model-gateway 0.2.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/dist/.tsbuildinfo +1 -0
- package/dist/capabilities.d.ts +26 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.data.d.ts +3 -0
- package/dist/capabilities.data.d.ts.map +1 -0
- package/dist/capabilities.data.js +5 -0
- package/dist/capabilities.js +169 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +733 -0
- package/dist/embedding.d.ts +38 -0
- package/dist/embedding.d.ts.map +1 -0
- package/dist/embedding.js +118 -0
- package/dist/gateway.d.ts +23 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +144 -0
- package/dist/http.d.ts +24 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +666 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/model-selection.d.ts +22 -0
- package/dist/model-selection.d.ts.map +1 -0
- package/dist/model-selection.js +59 -0
- package/dist/normalize.d.ts +9 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/normalize.js +114 -0
- package/dist/openai-adapter.d.ts +22 -0
- package/dist/openai-adapter.d.ts.map +1 -0
- package/dist/openai-adapter.js +382 -0
- package/dist/openai-embedding-adapter.d.ts +46 -0
- package/dist/openai-embedding-adapter.d.ts.map +1 -0
- package/dist/openai-embedding-adapter.js +271 -0
- package/dist/promptEnhancer/__tests__/_support.d.ts +15 -0
- package/dist/promptEnhancer/__tests__/_support.d.ts.map +1 -0
- package/dist/promptEnhancer/__tests__/_support.js +28 -0
- package/dist/promptEnhancer/__tests__/fixtures.d.ts +8 -0
- package/dist/promptEnhancer/__tests__/fixtures.d.ts.map +1 -0
- package/dist/promptEnhancer/__tests__/fixtures.js +58 -0
- package/dist/promptEnhancer/__tests__/grounding-fixtures.d.ts +11 -0
- package/dist/promptEnhancer/__tests__/grounding-fixtures.d.ts.map +1 -0
- package/dist/promptEnhancer/__tests__/grounding-fixtures.js +84 -0
- package/dist/promptEnhancer/candidates.d.ts +32 -0
- package/dist/promptEnhancer/candidates.d.ts.map +1 -0
- package/dist/promptEnhancer/candidates.js +109 -0
- package/dist/promptEnhancer/critic.d.ts +22 -0
- package/dist/promptEnhancer/critic.d.ts.map +1 -0
- package/dist/promptEnhancer/critic.js +237 -0
- package/dist/promptEnhancer/generator.d.ts +15 -0
- package/dist/promptEnhancer/generator.d.ts.map +1 -0
- package/dist/promptEnhancer/generator.js +424 -0
- package/dist/promptEnhancer/index.d.ts +16 -0
- package/dist/promptEnhancer/index.d.ts.map +1 -0
- package/dist/promptEnhancer/index.js +15 -0
- package/dist/promptEnhancer/optimize.d.ts +27 -0
- package/dist/promptEnhancer/optimize.d.ts.map +1 -0
- package/dist/promptEnhancer/optimize.js +203 -0
- package/dist/promptEnhancer/planner.d.ts +36 -0
- package/dist/promptEnhancer/planner.d.ts.map +1 -0
- package/dist/promptEnhancer/planner.js +55 -0
- package/dist/promptEnhancer/profiles.d.ts +20 -0
- package/dist/promptEnhancer/profiles.d.ts.map +1 -0
- package/dist/promptEnhancer/profiles.js +126 -0
- package/dist/promptEnhancer/rendering.d.ts +15 -0
- package/dist/promptEnhancer/rendering.d.ts.map +1 -0
- package/dist/promptEnhancer/rendering.js +72 -0
- package/dist/promptEnhancer/validate.d.ts +31 -0
- package/dist/promptEnhancer/validate.d.ts.map +1 -0
- package/dist/promptEnhancer/validate.js +144 -0
- package/dist/qualityIntelligence/budget.d.ts +10 -0
- package/dist/qualityIntelligence/budget.d.ts.map +1 -0
- package/dist/qualityIntelligence/budget.js +38 -0
- package/dist/qualityIntelligence/cancellation.d.ts +7 -0
- package/dist/qualityIntelligence/cancellation.d.ts.map +1 -0
- package/dist/qualityIntelligence/cancellation.js +58 -0
- package/dist/qualityIntelligence/capabilityGate.d.ts +13 -0
- package/dist/qualityIntelligence/capabilityGate.d.ts.map +1 -0
- package/dist/qualityIntelligence/capabilityGate.js +51 -0
- package/dist/qualityIntelligence/capabilityMapping.d.ts +4 -0
- package/dist/qualityIntelligence/capabilityMapping.d.ts.map +1 -0
- package/dist/qualityIntelligence/capabilityMapping.js +21 -0
- package/dist/qualityIntelligence/circuitBreaker.d.ts +26 -0
- package/dist/qualityIntelligence/circuitBreaker.d.ts.map +1 -0
- package/dist/qualityIntelligence/circuitBreaker.js +78 -0
- package/dist/qualityIntelligence/dispatcher.d.ts +38 -0
- package/dist/qualityIntelligence/dispatcher.d.ts.map +1 -0
- package/dist/qualityIntelligence/dispatcher.js +116 -0
- package/dist/qualityIntelligence/index.d.ts +20 -0
- package/dist/qualityIntelligence/index.d.ts.map +1 -0
- package/dist/qualityIntelligence/index.js +15 -0
- package/dist/qualityIntelligence/promptSegmentation.d.ts +13 -0
- package/dist/qualityIntelligence/promptSegmentation.d.ts.map +1 -0
- package/dist/qualityIntelligence/promptSegmentation.js +70 -0
- package/dist/qualityIntelligence/replayCache.d.ts +11 -0
- package/dist/qualityIntelligence/replayCache.d.ts.map +1 -0
- package/dist/qualityIntelligence/replayCache.js +72 -0
- package/dist/qualityIntelligence/routing.d.ts +11 -0
- package/dist/qualityIntelligence/routing.d.ts.map +1 -0
- package/dist/qualityIntelligence/routing.js +25 -0
- package/dist/qualityIntelligence/safeError.d.ts +38 -0
- package/dist/qualityIntelligence/safeError.d.ts.map +1 -0
- package/dist/qualityIntelligence/safeError.js +63 -0
- package/dist/qualityIntelligence/taskProfiles.d.ts +15 -0
- package/dist/qualityIntelligence/taskProfiles.d.ts.map +1 -0
- package/dist/qualityIntelligence/taskProfiles.js +101 -0
- package/dist/resilience.d.ts +26 -0
- package/dist/resilience.d.ts.map +1 -0
- package/dist/resilience.js +182 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/package.json +47 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Prompt Enhancer validate stage (Epic #1307, Issue #1313; ADR-0044 §4/§5/§7).
|
|
2
|
+
//
|
|
3
|
+
// The validate stage is the authority gate of the enhancer lifecycle. It composes two deterministic
|
|
4
|
+
// signals into one wire-safe `PromptSafetyAssessment`:
|
|
5
|
+
// 1. the STRUCTURAL assessment from `keiko-contracts` (required safeguards present; AC1/AC2/AC5,
|
|
6
|
+
// no-authority, no-disclosure, output validation; trusted-section defense in depth), and
|
|
7
|
+
// 2. the AUTHORITATIVE text-level detection from `keiko-security` (`detectPromptInjectionSignals`)
|
|
8
|
+
// run over (a) the candidate's TRUSTED sections — any injected/override/authority/secret content
|
|
9
|
+
// there is a blocking rejection (AC3), and (b) the UNTRUSTED user input — recorded for the audit
|
|
10
|
+
// trail and escalated to human review when an attack is detected, but never blocking, because the
|
|
11
|
+
// generated prompt isolates the input as data (AC2: untrusted content cannot override).
|
|
12
|
+
//
|
|
13
|
+
// The model gateway is allowed to depend on {contracts, security} (ADR-0019), so this is the natural
|
|
14
|
+
// home for the stage that needs both — mirroring how the QI dispatch primitives live here. The stage
|
|
15
|
+
// makes NO model call and grants NO authority (ADR-0044 §4): it produces an assessment artefact only.
|
|
16
|
+
//
|
|
17
|
+
// Determinism: pure. No IO, clock, or randomness.
|
|
18
|
+
import { assessEnhancedPromptStructuralSafety, summarizePromptSafety, PROMPT_SAFETY_VIOLATION_DETAILS, } from "@oscharko-dev/keiko-contracts";
|
|
19
|
+
import { detectPromptInjectionSignals, } from "@oscharko-dev/keiko-security";
|
|
20
|
+
const SIGNAL_MAPPING = {
|
|
21
|
+
"instruction-override": {
|
|
22
|
+
code: "untrusted-instruction-override",
|
|
23
|
+
ruleId: "no-manipulative-or-injected-instructions",
|
|
24
|
+
},
|
|
25
|
+
"system-prompt-disclosure": {
|
|
26
|
+
code: "system-prompt-disclosure",
|
|
27
|
+
ruleId: "no-secret-or-system-prompt-disclosure",
|
|
28
|
+
},
|
|
29
|
+
"secret-exfiltration": {
|
|
30
|
+
code: "secret-request",
|
|
31
|
+
ruleId: "no-secret-or-system-prompt-disclosure",
|
|
32
|
+
},
|
|
33
|
+
"tool-authority-request": { code: "capability-grant-claim", ruleId: "no-authority-grant" },
|
|
34
|
+
"egress-request": { code: "capability-grant-claim", ruleId: "no-authority-grant" },
|
|
35
|
+
"manipulative-framing": {
|
|
36
|
+
code: "manipulative-instruction",
|
|
37
|
+
ruleId: "no-manipulative-or-injected-instructions",
|
|
38
|
+
},
|
|
39
|
+
"embedded-secret-material": {
|
|
40
|
+
code: "secret-request",
|
|
41
|
+
ruleId: "no-secret-or-system-prompt-disclosure",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
const SEVERITY_RANK = {
|
|
45
|
+
info: 0,
|
|
46
|
+
warning: 1,
|
|
47
|
+
blocking: 2,
|
|
48
|
+
};
|
|
49
|
+
// The trusted instruction sections of a prompt, joined for the security scan. Excludes `input`, which
|
|
50
|
+
// is the untrusted channel scanned separately.
|
|
51
|
+
function trustedSectionsText(prompt) {
|
|
52
|
+
return [
|
|
53
|
+
prompt.role,
|
|
54
|
+
prompt.goal,
|
|
55
|
+
...prompt.context,
|
|
56
|
+
...prompt.taskDecomposition,
|
|
57
|
+
...prompt.constraints,
|
|
58
|
+
...prompt.groundingRules,
|
|
59
|
+
...prompt.qualityCriteria,
|
|
60
|
+
...prompt.uncertaintyHandling,
|
|
61
|
+
...prompt.safetyRules,
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|
|
64
|
+
function findingFor(signal, severity) {
|
|
65
|
+
const mapping = SIGNAL_MAPPING[signal.code];
|
|
66
|
+
return {
|
|
67
|
+
code: mapping.code,
|
|
68
|
+
ruleId: mapping.ruleId,
|
|
69
|
+
severity,
|
|
70
|
+
detail: PROMPT_SAFETY_VIOLATION_DETAILS[mapping.code],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Deduplicate findings by violation code, keeping the most severe instance. Keeps the assessment
|
|
74
|
+
// auditable without redundant rows when both the structural and security passes flag the same concern.
|
|
75
|
+
function dedupeFindings(findings) {
|
|
76
|
+
const byCode = new Map();
|
|
77
|
+
for (const finding of findings) {
|
|
78
|
+
const existing = byCode.get(finding.code);
|
|
79
|
+
if (existing === undefined ||
|
|
80
|
+
SEVERITY_RANK[finding.severity] > SEVERITY_RANK[existing.severity]) {
|
|
81
|
+
byCode.set(finding.code, finding);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return [...byCode.values()];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Assess the safety of one Enhanced Prompt against the full validate-stage rule set. Pure. Combines
|
|
88
|
+
* the contracts structural assessment with the keiko-security text detector over the trusted sections
|
|
89
|
+
* (blocking on any injected/authority/secret content — AC3) and over the untrusted input (recorded as
|
|
90
|
+
* audit evidence; escalates to human review on a critical attack but never blocks — AC2). Returns a
|
|
91
|
+
* wire-safe `PromptSafetyAssessment` that is content-free and grants no authority.
|
|
92
|
+
*/
|
|
93
|
+
export function assessPromptSafety(args) {
|
|
94
|
+
const { prompt, analysis, input } = args;
|
|
95
|
+
const structural = assessEnhancedPromptStructuralSafety(prompt, analysis);
|
|
96
|
+
// (a) Trusted-section scan: any unsafe signal here is a blocking rejection (a safe generated prompt
|
|
97
|
+
// never contains it; a forged/tampered candidate would).
|
|
98
|
+
const trustedFindings = detectPromptInjectionSignals(trustedSectionsText(prompt)).map((signal) => findingFor(signal, "blocking"));
|
|
99
|
+
// (b) Untrusted-input scan: recorded for the audit trail. Critical attack attempts escalate to human
|
|
100
|
+
// review; nothing here is blocking because the prompt isolates the input as data.
|
|
101
|
+
const inputSignals = detectPromptInjectionSignals(input.text);
|
|
102
|
+
const inputFindings = inputSignals.map((signal) => findingFor(signal, signal.severity === "critical" ? "warning" : "info"));
|
|
103
|
+
const inputAttackDetected = inputSignals.some((signal) => signal.severity === "critical");
|
|
104
|
+
const findings = dedupeFindings([...structural.findings, ...trustedFindings, ...inputFindings]);
|
|
105
|
+
const requiresHumanReview = structural.requiresHumanReview || inputAttackDetected;
|
|
106
|
+
const { decision, verificationStatus } = summarizePromptSafety(findings, requiresHumanReview);
|
|
107
|
+
const leastPrivilege = withHumanApproval(structural.leastPrivilege, requiresHumanReview);
|
|
108
|
+
return {
|
|
109
|
+
schemaVersion: structural.schemaVersion,
|
|
110
|
+
promptId: prompt.promptId,
|
|
111
|
+
decision,
|
|
112
|
+
requiresHumanReview,
|
|
113
|
+
verificationStatus,
|
|
114
|
+
findings,
|
|
115
|
+
leastPrivilege,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function withHumanApproval(base, requiresHumanReview) {
|
|
119
|
+
if (!requiresHumanReview || base.includes("require-human-approval")) {
|
|
120
|
+
return base;
|
|
121
|
+
}
|
|
122
|
+
return [...base, "require-human-approval"];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Screen a generated candidate set through the validate stage (AC3 — candidates that add hidden
|
|
126
|
+
* assumptions, unapproved tool actions, secret requests, or manipulative instructions are rejected).
|
|
127
|
+
* Pure. Each candidate is assessed independently; the result partitions them into a usable `safe` set
|
|
128
|
+
* (decision !== "rejected") and a `rejected` set, each carrying its assessment for the audit trail.
|
|
129
|
+
*/
|
|
130
|
+
export function screenCandidatesForSafety(candidates, analysis, input) {
|
|
131
|
+
const safe = [];
|
|
132
|
+
const rejected = [];
|
|
133
|
+
for (const candidate of candidates) {
|
|
134
|
+
const assessment = assessPromptSafety({ prompt: candidate.prompt, analysis, input });
|
|
135
|
+
const screened = { candidate, assessment };
|
|
136
|
+
if (assessment.decision === "rejected") {
|
|
137
|
+
rejected.push(screened);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
safe.push(screened);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { safe, rejected };
|
|
144
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface QualityIntelligenceBudgetState {
|
|
2
|
+
readonly totalBudget: number;
|
|
3
|
+
readonly consumed: number;
|
|
4
|
+
}
|
|
5
|
+
export declare function createBudget(totalBudget: number): QualityIntelligenceBudgetState;
|
|
6
|
+
export declare function reserveBudget(state: QualityIntelligenceBudgetState, cost: number): QualityIntelligenceBudgetState;
|
|
7
|
+
export declare function releaseBudget(state: QualityIntelligenceBudgetState, refund: number): QualityIntelligenceBudgetState;
|
|
8
|
+
export declare function isExhausted(state: QualityIntelligenceBudgetState): boolean;
|
|
9
|
+
export declare function remainingBudget(state: QualityIntelligenceBudgetState): number;
|
|
10
|
+
//# sourceMappingURL=budget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.d.ts","sourceRoot":"","sources":["../../src/qualityIntelligence/budget.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,8BAA8B,CAGhF;AASD,wBAAgB,aAAa,CAC3B,KAAK,EAAE,8BAA8B,EACrC,IAAI,EAAE,MAAM,GACX,8BAA8B,CAOhC;AAED,wBAAgB,aAAa,CAC3B,KAAK,EAAE,8BAA8B,EACrC,MAAM,EAAE,MAAM,GACb,8BAA8B,CAOhC;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,8BAA8B,GAAG,OAAO,CAE1E;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,8BAA8B,GAAG,MAAM,CAE7E"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Quality Intelligence token-budget accounting (Epic #270, Issue #279).
|
|
2
|
+
//
|
|
3
|
+
// Pure-function budget state machine: reserve, release, exhaustion check. State is an
|
|
4
|
+
// immutable shape — every mutating call returns a NEW state value. Negative reservations
|
|
5
|
+
// and refunds are clamped to zero so callers cannot accidentally inflate or underflow the
|
|
6
|
+
// budget.
|
|
7
|
+
export function createBudget(totalBudget) {
|
|
8
|
+
const clamped = Number.isFinite(totalBudget) && totalBudget > 0 ? totalBudget : 0;
|
|
9
|
+
return Object.freeze({ totalBudget: clamped, consumed: 0 });
|
|
10
|
+
}
|
|
11
|
+
function clampCost(cost) {
|
|
12
|
+
if (!Number.isFinite(cost) || cost <= 0) {
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
return cost;
|
|
16
|
+
}
|
|
17
|
+
export function reserveBudget(state, cost) {
|
|
18
|
+
const delta = clampCost(cost);
|
|
19
|
+
const nextConsumed = Math.min(state.totalBudget, state.consumed + delta);
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
totalBudget: state.totalBudget,
|
|
22
|
+
consumed: nextConsumed,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function releaseBudget(state, refund) {
|
|
26
|
+
const delta = clampCost(refund);
|
|
27
|
+
const nextConsumed = Math.max(0, state.consumed - delta);
|
|
28
|
+
return Object.freeze({
|
|
29
|
+
totalBudget: state.totalBudget,
|
|
30
|
+
consumed: nextConsumed,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function isExhausted(state) {
|
|
34
|
+
return state.consumed >= state.totalBudget;
|
|
35
|
+
}
|
|
36
|
+
export function remainingBudget(state) {
|
|
37
|
+
return Math.max(0, state.totalBudget - state.consumed);
|
|
38
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface QualityIntelligenceCancellationHandle {
|
|
2
|
+
readonly signal: AbortSignal;
|
|
3
|
+
readonly reasonKind: () => "timeout" | "external" | "none";
|
|
4
|
+
readonly dispose: () => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function composeCancellationSignal(timeoutMs: number, external: AbortSignal | undefined): QualityIntelligenceCancellationHandle;
|
|
7
|
+
//# sourceMappingURL=cancellation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cancellation.d.ts","sourceRoot":"","sources":["../../src/qualityIntelligence/cancellation.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,qCAAqC;IACpD,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,SAAS,GAAG,UAAU,GAAG,MAAM,CAAC;IAC3D,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC9B;AAsDD,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,WAAW,GAAG,SAAS,GAChC,qCAAqC,CAcvC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Quality Intelligence cancellation composition (Epic #270, Issue #279).
|
|
2
|
+
//
|
|
3
|
+
// Composes an external AbortSignal with an internal profile-timeout into a single effective
|
|
4
|
+
// signal. The dispatcher hands the resulting signal to the underlying ModelPort so it can
|
|
5
|
+
// abort the in-flight call on EITHER the timeout firing OR the external signal aborting.
|
|
6
|
+
// No timers leak: the caller MUST invoke `dispose()` once the call settles.
|
|
7
|
+
function attachExternal(external, controller, reason) {
|
|
8
|
+
if (external === undefined) {
|
|
9
|
+
return () => {
|
|
10
|
+
/* nothing to detach */
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
if (external.aborted) {
|
|
14
|
+
reason.kind = "external";
|
|
15
|
+
controller.abort();
|
|
16
|
+
return () => {
|
|
17
|
+
/* already aborted */
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const onAbort = () => {
|
|
21
|
+
reason.kind = "external";
|
|
22
|
+
controller.abort();
|
|
23
|
+
};
|
|
24
|
+
external.addEventListener("abort", onAbort, { once: true });
|
|
25
|
+
return () => {
|
|
26
|
+
external.removeEventListener("abort", onAbort);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function attachTimeout(timeoutMs, controller, reason) {
|
|
30
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
31
|
+
return () => {
|
|
32
|
+
/* no timeout requested */
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const timer = setTimeout(() => {
|
|
36
|
+
if (reason.kind === "none") {
|
|
37
|
+
reason.kind = "timeout";
|
|
38
|
+
}
|
|
39
|
+
controller.abort();
|
|
40
|
+
}, timeoutMs);
|
|
41
|
+
return () => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function composeCancellationSignal(timeoutMs, external) {
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
const reason = { kind: "none" };
|
|
48
|
+
const detachExternal = attachExternal(external, controller, reason);
|
|
49
|
+
const detachTimeout = attachTimeout(timeoutMs, controller, reason);
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
signal: controller.signal,
|
|
52
|
+
reasonKind: () => reason.kind,
|
|
53
|
+
dispose: () => {
|
|
54
|
+
detachTimeout();
|
|
55
|
+
detachExternal();
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ModelCapability } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
import type { ModelSelectionQuery } from "../model-selection.js";
|
|
3
|
+
import type { QualityIntelligenceCapability, QualityIntelligenceTaskProfile } from "./taskProfiles.js";
|
|
4
|
+
/**
|
|
5
|
+
* Project a profile's required QI capabilities onto the gateway `ModelSelectionQuery` constraints
|
|
6
|
+
* the selector understands. This is the WRITE side of the SAME capability mapping that `modelSupports`
|
|
7
|
+
* READS, kept in one file so auto-selection and the gate can never drift (Issue #762: a single shared
|
|
8
|
+
* capability mapping, never a per-stage duplicate). text → chat kind (every QI task profile is a chat
|
|
9
|
+
* task); structured-output → structuredOutput; function-calling → toolCalling; vision → supportsImageInput.
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildSelectionQueryForCapabilities(required: readonly QualityIntelligenceCapability[]): ModelSelectionQuery;
|
|
12
|
+
export declare function assertProfileCompatibleWithModel(profile: QualityIntelligenceTaskProfile, modelCapability: ModelCapability): void;
|
|
13
|
+
//# sourceMappingURL=capabilityGate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilityGate.d.ts","sourceRoot":"","sources":["../../src/qualityIntelligence/capabilityGate.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAGrE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,KAAK,EACV,6BAA6B,EAC7B,8BAA8B,EAC/B,MAAM,mBAAmB,CAAC;AAE3B;;;;;;GAMG;AACH,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,SAAS,6BAA6B,EAAE,GACjD,mBAAmB,CAyBrB;AAED,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,8BAA8B,EACvC,eAAe,EAAE,eAAe,GAC/B,IAAI,CAWN"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Quality Intelligence capability gate (Epic #270, Issue #279).
|
|
2
|
+
//
|
|
3
|
+
// Asserts that a model's capability record satisfies the structural requirements of a QI
|
|
4
|
+
// task profile. Throws a safe-error exception (no inputs, no secrets) on mismatch.
|
|
5
|
+
import { QualityIntelligenceSafeErrorException, makeCapabilityMismatchError } from "./safeError.js";
|
|
6
|
+
import { modelSupportsCapability } from "./capabilityMapping.js";
|
|
7
|
+
/**
|
|
8
|
+
* Project a profile's required QI capabilities onto the gateway `ModelSelectionQuery` constraints
|
|
9
|
+
* the selector understands. This is the WRITE side of the SAME capability mapping that `modelSupports`
|
|
10
|
+
* READS, kept in one file so auto-selection and the gate can never drift (Issue #762: a single shared
|
|
11
|
+
* capability mapping, never a per-stage duplicate). text → chat kind (every QI task profile is a chat
|
|
12
|
+
* task); structured-output → structuredOutput; function-calling → toolCalling; vision → supportsImageInput.
|
|
13
|
+
*/
|
|
14
|
+
export function buildSelectionQueryForCapabilities(required) {
|
|
15
|
+
let structuredOutput;
|
|
16
|
+
let toolCalling;
|
|
17
|
+
let supportsImageInput;
|
|
18
|
+
for (const capability of required) {
|
|
19
|
+
switch (capability) {
|
|
20
|
+
case "text":
|
|
21
|
+
break;
|
|
22
|
+
case "structured-output":
|
|
23
|
+
structuredOutput = true;
|
|
24
|
+
break;
|
|
25
|
+
case "function-calling":
|
|
26
|
+
toolCalling = true;
|
|
27
|
+
break;
|
|
28
|
+
case "vision":
|
|
29
|
+
supportsImageInput = true;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
kind: "chat",
|
|
35
|
+
...(structuredOutput !== undefined ? { structuredOutput } : {}),
|
|
36
|
+
...(toolCalling !== undefined ? { toolCalling } : {}),
|
|
37
|
+
...(supportsImageInput !== undefined ? { supportsImageInput } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function assertProfileCompatibleWithModel(profile, modelCapability) {
|
|
41
|
+
const missing = [];
|
|
42
|
+
for (const required of profile.requiredCapabilities) {
|
|
43
|
+
if (!modelSupportsCapability(required, modelCapability)) {
|
|
44
|
+
missing.push(required);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (missing.length === 0) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
throw new QualityIntelligenceSafeErrorException(makeCapabilityMismatchError(profile.id, missing));
|
|
51
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ModelCapability } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
import type { QualityIntelligenceCapability } from "./taskProfiles.js";
|
|
3
|
+
export declare function modelSupportsCapability(capability: QualityIntelligenceCapability, model: ModelCapability): boolean;
|
|
4
|
+
//# sourceMappingURL=capabilityMapping.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilityMapping.d.ts","sourceRoot":"","sources":["../../src/qualityIntelligence/capabilityMapping.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,mBAAmB,CAAC;AAEvE,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,6BAA6B,EACzC,KAAK,EAAE,eAAe,GACrB,OAAO,CAWT"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Quality Intelligence capability mapping (Epic #270, Issue #279; consolidated under Epic #761).
|
|
2
|
+
//
|
|
3
|
+
// The single source of truth for the READ side of the QI-capability ⇄ model-capability mapping:
|
|
4
|
+
// "does a model satisfy a required QI capability?". Both the capability gate
|
|
5
|
+
// (assertProfileCompatibleWithModel) and the deterministic profile router (selectModelForProfile)
|
|
6
|
+
// consume this one predicate, so they can never disagree on what a capability means. The WRITE-side
|
|
7
|
+
// counterpart that projects required capabilities onto a gateway ModelSelectionQuery is
|
|
8
|
+
// buildSelectionQueryForCapabilities (capabilityGate.ts), kept consistent with this READ side by the
|
|
9
|
+
// capabilityGate tests.
|
|
10
|
+
export function modelSupportsCapability(capability, model) {
|
|
11
|
+
switch (capability) {
|
|
12
|
+
case "text":
|
|
13
|
+
return model.kind === "chat";
|
|
14
|
+
case "vision":
|
|
15
|
+
return model.supportsImageInput;
|
|
16
|
+
case "structured-output":
|
|
17
|
+
return model.structuredOutput;
|
|
18
|
+
case "function-calling":
|
|
19
|
+
return model.toolCalling;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type QualityIntelligenceCircuitState = "closed" | "open" | "half-open";
|
|
2
|
+
export interface QualityIntelligenceCircuitBreakerState {
|
|
3
|
+
readonly state: QualityIntelligenceCircuitState;
|
|
4
|
+
readonly consecutiveFailures: number;
|
|
5
|
+
readonly openedAtMs: number | null;
|
|
6
|
+
}
|
|
7
|
+
export interface QualityIntelligenceCircuitBreakerConfig {
|
|
8
|
+
readonly failureThreshold: number;
|
|
9
|
+
readonly cooldownMs: number;
|
|
10
|
+
readonly halfOpenProbes: number;
|
|
11
|
+
}
|
|
12
|
+
export declare const DEFAULT_QUALITY_INTELLIGENCE_CIRCUIT_BREAKER_CONFIG: QualityIntelligenceCircuitBreakerConfig;
|
|
13
|
+
export declare function createCircuitBreakerState(): QualityIntelligenceCircuitBreakerState;
|
|
14
|
+
export type QualityIntelligenceCircuitEvent = {
|
|
15
|
+
readonly kind: "success";
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: "failure";
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: "probe";
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: "tick";
|
|
22
|
+
readonly nowMs: number;
|
|
23
|
+
};
|
|
24
|
+
export declare function transitionOn(state: QualityIntelligenceCircuitBreakerState, event: QualityIntelligenceCircuitEvent, config?: QualityIntelligenceCircuitBreakerConfig, nowMs?: number): QualityIntelligenceCircuitBreakerState;
|
|
25
|
+
export declare function shouldAttempt(state: QualityIntelligenceCircuitBreakerState): boolean;
|
|
26
|
+
//# sourceMappingURL=circuitBreaker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuitBreaker.d.ts","sourceRoot":"","sources":["../../src/qualityIntelligence/circuitBreaker.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,+BAA+B,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAE9E,MAAM,WAAW,sCAAsC;IACrD,QAAQ,CAAC,KAAK,EAAE,+BAA+B,CAAC;IAChD,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,uCAAuC;IACtD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,eAAO,MAAM,mDAAmD,EAAE,uCAK9D,CAAC;AAEL,wBAAgB,yBAAyB,IAAI,sCAAsC,CAMlF;AAED,MAAM,MAAM,+BAA+B,GACvC;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC1B;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AA6DtD,wBAAgB,YAAY,CAC1B,KAAK,EAAE,sCAAsC,EAC7C,KAAK,EAAE,+BAA+B,EACtC,MAAM,GAAE,uCAA6F,EACrG,KAAK,SAAI,GACR,sCAAsC,CAWxC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,sCAAsC,GAAG,OAAO,CAEpF"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Quality Intelligence circuit breaker (Epic #270, Issue #279).
|
|
2
|
+
//
|
|
3
|
+
// Pure half-open state machine. The dispatcher records each model call outcome via
|
|
4
|
+
// `transitionOn` and consults `state` to decide whether to attempt or skip a call. State is
|
|
5
|
+
// immutable; every transition returns a new value. Thresholds are configurable but default
|
|
6
|
+
// to a frozen constant tuned for QI workloads.
|
|
7
|
+
export const DEFAULT_QUALITY_INTELLIGENCE_CIRCUIT_BREAKER_CONFIG = Object.freeze({
|
|
8
|
+
failureThreshold: 4,
|
|
9
|
+
cooldownMs: 30_000,
|
|
10
|
+
halfOpenProbes: 1,
|
|
11
|
+
});
|
|
12
|
+
export function createCircuitBreakerState() {
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
state: "closed",
|
|
15
|
+
consecutiveFailures: 0,
|
|
16
|
+
openedAtMs: null,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function onSuccess() {
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
state: "closed",
|
|
22
|
+
consecutiveFailures: 0,
|
|
23
|
+
openedAtMs: null,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function onFailure(state, config, nowMs) {
|
|
27
|
+
const nextFailures = state.consecutiveFailures + 1;
|
|
28
|
+
if (nextFailures >= config.failureThreshold) {
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
state: "open",
|
|
31
|
+
consecutiveFailures: nextFailures,
|
|
32
|
+
openedAtMs: nowMs,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
state: state.state === "half-open" ? "open" : state.state,
|
|
37
|
+
consecutiveFailures: nextFailures,
|
|
38
|
+
openedAtMs: state.state === "half-open" ? nowMs : state.openedAtMs,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function onProbe(state) {
|
|
42
|
+
if (state.state === "open") {
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
state: "half-open",
|
|
45
|
+
consecutiveFailures: state.consecutiveFailures,
|
|
46
|
+
openedAtMs: state.openedAtMs,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return state;
|
|
50
|
+
}
|
|
51
|
+
function onTick(state, config, nowMs) {
|
|
52
|
+
if (state.state !== "open" || state.openedAtMs === null) {
|
|
53
|
+
return state;
|
|
54
|
+
}
|
|
55
|
+
if (nowMs - state.openedAtMs >= config.cooldownMs) {
|
|
56
|
+
return Object.freeze({
|
|
57
|
+
state: "half-open",
|
|
58
|
+
consecutiveFailures: state.consecutiveFailures,
|
|
59
|
+
openedAtMs: state.openedAtMs,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return state;
|
|
63
|
+
}
|
|
64
|
+
export function transitionOn(state, event, config = DEFAULT_QUALITY_INTELLIGENCE_CIRCUIT_BREAKER_CONFIG, nowMs = 0) {
|
|
65
|
+
switch (event.kind) {
|
|
66
|
+
case "success":
|
|
67
|
+
return onSuccess();
|
|
68
|
+
case "failure":
|
|
69
|
+
return onFailure(state, config, nowMs);
|
|
70
|
+
case "probe":
|
|
71
|
+
return onProbe(state);
|
|
72
|
+
case "tick":
|
|
73
|
+
return onTick(state, config, event.nowMs);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function shouldAttempt(state) {
|
|
77
|
+
return state.state !== "open";
|
|
78
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ModelCapability, NormalizedResponse } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
import type { QualityIntelligenceBudgetState } from "./budget.js";
|
|
3
|
+
import { type QualityIntelligenceUntrustedEvidenceInput } from "./promptSegmentation.js";
|
|
4
|
+
import { type QualityIntelligenceReplayCachePort } from "./replayCache.js";
|
|
5
|
+
import type { QualityIntelligenceTaskProfile } from "./taskProfiles.js";
|
|
6
|
+
import type { ModelProviderConfig, ProviderAdapter } from "../types.js";
|
|
7
|
+
export interface QualityIntelligenceDispatcherArgs {
|
|
8
|
+
readonly profile: QualityIntelligenceTaskProfile;
|
|
9
|
+
readonly instruction: string;
|
|
10
|
+
readonly evidence: readonly QualityIntelligenceUntrustedEvidenceInput[];
|
|
11
|
+
readonly model: ModelCapability;
|
|
12
|
+
readonly providerConfig: ModelProviderConfig;
|
|
13
|
+
readonly port: ProviderAdapter;
|
|
14
|
+
readonly cache: QualityIntelligenceReplayCachePort<NormalizedResponse>;
|
|
15
|
+
readonly budget: QualityIntelligenceBudgetState;
|
|
16
|
+
readonly signal?: AbortSignal | undefined;
|
|
17
|
+
}
|
|
18
|
+
export interface QualityIntelligenceDispatcherResult {
|
|
19
|
+
readonly response: NormalizedResponse;
|
|
20
|
+
readonly budget: QualityIntelligenceBudgetState;
|
|
21
|
+
readonly cacheHit: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Compose a single Quality Intelligence model call with capability gate, budget reservation,
|
|
25
|
+
* prompt segmentation, replay cache, composed timeout/cancellation, and qi/* safe-error shaping.
|
|
26
|
+
*
|
|
27
|
+
* REUSABLE PRIMITIVE — NOT the current live path. The live generation/judge runtime
|
|
28
|
+
* (`keiko-server` generationPort.ts / judgePort.ts) deliberately calls the gateway `ModelPort`
|
|
29
|
+
* directly and applies its own capability gate + prompt segmentation, inheriting timeout / retry /
|
|
30
|
+
* circuit-breaking from the gateway resilience layer (`resilience.ts`) — the same machinery Chat
|
|
31
|
+
* and grounded-QA use. This dispatcher (and its budget / replayCache / circuitBreaker / cancellation
|
|
32
|
+
* helpers) is the ported-from-Test-Intelligence composition kept available for a future multi-call
|
|
33
|
+
* QI flow that wants combined budget + cache + circuit semantics at one call site. It must NOT be
|
|
34
|
+
* force-wired into the live ports (that would change the resilience contract) and must NOT be
|
|
35
|
+
* deleted as "dead code" (#279: ported TI ideas live here as generic gateway capabilities).
|
|
36
|
+
*/
|
|
37
|
+
export declare function dispatchQualityIntelligenceRequest(args: QualityIntelligenceDispatcherArgs): Promise<QualityIntelligenceDispatcherResult>;
|
|
38
|
+
//# sourceMappingURL=dispatcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/qualityIntelligence/dispatcher.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAGV,eAAe,EACf,kBAAkB,EACnB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,aAAa,CAAC;AAElE,OAAO,EAGL,KAAK,yCAAyC,EAC/C,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAGL,KAAK,kCAAkC,EACxC,MAAM,kBAAkB,CAAC;AAQ1B,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,KAAK,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAExE,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,OAAO,EAAE,8BAA8B,CAAC;IACjD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,yCAAyC,EAAE,CAAC;IACxE,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,QAAQ,CAAC,cAAc,EAAE,mBAAmB,CAAC;IAC7C,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,kCAAkC,CAAC,kBAAkB,CAAC,CAAC;IACvE,QAAQ,CAAC,MAAM,EAAE,8BAA8B,CAAC;IAChD,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CAC3C;AAED,MAAM,WAAW,mCAAmC;IAClD,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,8BAA8B,CAAC;IAChD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAuED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,kCAAkC,CACtD,IAAI,EAAE,iCAAiC,GACtC,OAAO,CAAC,mCAAmC,CAAC,CAuC9C"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Quality Intelligence dispatcher (Epic #270, Issue #279).
|
|
2
|
+
//
|
|
3
|
+
// Routes a Quality Intelligence model call through the existing gateway ModelPort. The
|
|
4
|
+
// dispatcher COMPOSES — it never instantiates a provider SDK. It performs (in order):
|
|
5
|
+
// 1. capability check (M1: capabilityGate)
|
|
6
|
+
// 2. budget reservation (M2: budget)
|
|
7
|
+
// 3. prompt segmentation (M1: promptSegmentation)
|
|
8
|
+
// 4. cache lookup (M2: replayCache)
|
|
9
|
+
// 5. wire-prompt assembly (this file, pure)
|
|
10
|
+
// 6. ModelPort.call(...) with composed signal and profile timeout (this file)
|
|
11
|
+
// 7. cache store (if cacheable) (M2: replayCache)
|
|
12
|
+
// 8. error -> qi/* safe-error shape (M1: safeError)
|
|
13
|
+
// The ModelPort itself is the gateway's existing ProviderAdapter — proven at the type
|
|
14
|
+
// level by the parameter signature.
|
|
15
|
+
import { composeCancellationSignal } from "./cancellation.js";
|
|
16
|
+
import { isExhausted, reserveBudget } from "./budget.js";
|
|
17
|
+
import { assertProfileCompatibleWithModel } from "./capabilityGate.js";
|
|
18
|
+
import { buildPromptSegments, } from "./promptSegmentation.js";
|
|
19
|
+
import { deriveReplayCacheKey, isCacheable, } from "./replayCache.js";
|
|
20
|
+
import { QualityIntelligenceSafeErrorException, makeBudgetExhaustedError, makeCancelledError, makeProviderError, makeTimeoutError, } from "./safeError.js";
|
|
21
|
+
function assembleMessages(segments) {
|
|
22
|
+
const evidenceBlock = segments.evidenceUntrusted.length === 0
|
|
23
|
+
? ""
|
|
24
|
+
: segments.evidenceUntrusted
|
|
25
|
+
.map((e) => `<qi-evidence kind="${e.kind}">${e.value}</qi-evidence>`)
|
|
26
|
+
.join("\n");
|
|
27
|
+
const userContent = evidenceBlock === ""
|
|
28
|
+
? segments.instructionTrusted
|
|
29
|
+
: `${segments.instructionTrusted}\n${evidenceBlock}`;
|
|
30
|
+
return Object.freeze([
|
|
31
|
+
Object.freeze({ role: "system", content: segments.systemTrusted }),
|
|
32
|
+
Object.freeze({ role: "user", content: userContent }),
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
function buildGatewayRequest(modelId, segments, signal) {
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
modelId,
|
|
38
|
+
messages: assembleMessages(segments),
|
|
39
|
+
stream: false,
|
|
40
|
+
cancellationSignal: signal,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function classifyAndThrow(profileId, timeoutMs, reasonKind, caught) {
|
|
44
|
+
if (caught instanceof QualityIntelligenceSafeErrorException) {
|
|
45
|
+
throw caught;
|
|
46
|
+
}
|
|
47
|
+
if (reasonKind === "timeout") {
|
|
48
|
+
throw new QualityIntelligenceSafeErrorException(makeTimeoutError(profileId, timeoutMs));
|
|
49
|
+
}
|
|
50
|
+
if (reasonKind === "external") {
|
|
51
|
+
throw new QualityIntelligenceSafeErrorException(makeCancelledError(profileId));
|
|
52
|
+
}
|
|
53
|
+
throw new QualityIntelligenceSafeErrorException(makeProviderError(profileId));
|
|
54
|
+
}
|
|
55
|
+
async function invokePort(ctx) {
|
|
56
|
+
const handle = composeCancellationSignal(ctx.profile.timeoutMsHint, ctx.externalSignal);
|
|
57
|
+
try {
|
|
58
|
+
const request = buildGatewayRequest(ctx.modelId, ctx.segments, handle.signal);
|
|
59
|
+
return await ctx.port.call(request, ctx.providerConfig);
|
|
60
|
+
}
|
|
61
|
+
catch (caught) {
|
|
62
|
+
classifyAndThrow(ctx.profile.id, ctx.profile.timeoutMsHint, handle.reasonKind(), caught);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
handle.dispose();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Compose a single Quality Intelligence model call with capability gate, budget reservation,
|
|
70
|
+
* prompt segmentation, replay cache, composed timeout/cancellation, and qi/* safe-error shaping.
|
|
71
|
+
*
|
|
72
|
+
* REUSABLE PRIMITIVE — NOT the current live path. The live generation/judge runtime
|
|
73
|
+
* (`keiko-server` generationPort.ts / judgePort.ts) deliberately calls the gateway `ModelPort`
|
|
74
|
+
* directly and applies its own capability gate + prompt segmentation, inheriting timeout / retry /
|
|
75
|
+
* circuit-breaking from the gateway resilience layer (`resilience.ts`) — the same machinery Chat
|
|
76
|
+
* and grounded-QA use. This dispatcher (and its budget / replayCache / circuitBreaker / cancellation
|
|
77
|
+
* helpers) is the ported-from-Test-Intelligence composition kept available for a future multi-call
|
|
78
|
+
* QI flow that wants combined budget + cache + circuit semantics at one call site. It must NOT be
|
|
79
|
+
* force-wired into the live ports (that would change the resilience contract) and must NOT be
|
|
80
|
+
* deleted as "dead code" (#279: ported TI ideas live here as generic gateway capabilities).
|
|
81
|
+
*/
|
|
82
|
+
export async function dispatchQualityIntelligenceRequest(args) {
|
|
83
|
+
assertProfileCompatibleWithModel(args.profile, args.model);
|
|
84
|
+
if (isExhausted(args.budget)) {
|
|
85
|
+
throw new QualityIntelligenceSafeErrorException(makeBudgetExhaustedError(args.profile.id));
|
|
86
|
+
}
|
|
87
|
+
const nextBudget = reserveBudget(args.budget, args.profile.tokenBudgetHint);
|
|
88
|
+
const segments = buildPromptSegments(args.profile, args.instruction, args.evidence);
|
|
89
|
+
const cacheKey = await deriveReplayCacheKey(args.profile, segments, args.model.id);
|
|
90
|
+
if (isCacheable(args.profile)) {
|
|
91
|
+
const cached = args.cache.get(cacheKey);
|
|
92
|
+
if (cached !== undefined) {
|
|
93
|
+
return Object.freeze({
|
|
94
|
+
response: cached,
|
|
95
|
+
budget: nextBudget,
|
|
96
|
+
cacheHit: true,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const response = await invokePort({
|
|
101
|
+
profile: args.profile,
|
|
102
|
+
port: args.port,
|
|
103
|
+
providerConfig: args.providerConfig,
|
|
104
|
+
modelId: args.model.id,
|
|
105
|
+
segments,
|
|
106
|
+
externalSignal: args.signal,
|
|
107
|
+
});
|
|
108
|
+
if (isCacheable(args.profile)) {
|
|
109
|
+
args.cache.set(cacheKey, response);
|
|
110
|
+
}
|
|
111
|
+
return Object.freeze({
|
|
112
|
+
response,
|
|
113
|
+
budget: nextBudget,
|
|
114
|
+
cacheHit: false,
|
|
115
|
+
});
|
|
116
|
+
}
|