@warmdrift/kgauto-compiler 2.0.0-alpha.57 → 2.0.0-alpha.59
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/chunk-B4INZ32V.mjs +185 -0
- package/dist/index.d.mts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +199 -6
- package/dist/index.mjs +19 -6
- package/dist/key-health.d.mts +121 -0
- package/dist/key-health.d.ts +121 -0
- package/dist/key-health.js +209 -0
- package/dist/key-health.mjs +6 -0
- package/package.json +7 -2
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// src/key-health.ts
|
|
2
|
+
var JSON_HEADERS = { "Content-Type": "application/json" };
|
|
3
|
+
function jsonResponse(status, body) {
|
|
4
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
|
|
5
|
+
}
|
|
6
|
+
function bearerOf(req) {
|
|
7
|
+
const header = req.headers.get("Authorization") ?? "";
|
|
8
|
+
const match = /^Bearer\s+(.+)$/i.exec(header);
|
|
9
|
+
return match?.[1]?.trim() ?? "";
|
|
10
|
+
}
|
|
11
|
+
function requireString(name, value) {
|
|
12
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
13
|
+
throw new Error(`createKeyHealthRoute: ${name} is required`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function envKey(env, name) {
|
|
18
|
+
const v = env[name];
|
|
19
|
+
if (typeof v !== "string") return void 0;
|
|
20
|
+
const trimmed = v.trim();
|
|
21
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
22
|
+
}
|
|
23
|
+
var PROBE_SPECS = [
|
|
24
|
+
{
|
|
25
|
+
provider: "anthropic",
|
|
26
|
+
canonicalEnvName: "ANTHROPIC_API_KEY",
|
|
27
|
+
buildRequest: (key) => ({
|
|
28
|
+
url: "https://api.anthropic.com/v1/models?limit=1",
|
|
29
|
+
headers: { "x-api-key": key, "anthropic-version": "2023-06-01" }
|
|
30
|
+
})
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
provider: "deepseek",
|
|
34
|
+
canonicalEnvName: "DEEPSEEK_API_KEY",
|
|
35
|
+
buildRequest: (key) => ({
|
|
36
|
+
url: "https://api.deepseek.com/user/balance",
|
|
37
|
+
headers: { Authorization: `Bearer ${key}` }
|
|
38
|
+
}),
|
|
39
|
+
parseBalanceUsd: (body) => {
|
|
40
|
+
const infos = body?.balance_infos;
|
|
41
|
+
if (!Array.isArray(infos) || infos.length === 0) return void 0;
|
|
42
|
+
const first = infos[0];
|
|
43
|
+
if (first?.currency !== "USD") return void 0;
|
|
44
|
+
const n = Number(first.total_balance);
|
|
45
|
+
return Number.isFinite(n) ? n : void 0;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
provider: "google",
|
|
50
|
+
canonicalEnvName: "GEMINI_API_KEY",
|
|
51
|
+
buildRequest: (key) => ({
|
|
52
|
+
url: `https://generativelanguage.googleapis.com/v1beta/models?pageSize=1&key=${encodeURIComponent(key)}`,
|
|
53
|
+
headers: {}
|
|
54
|
+
})
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
provider: "openai",
|
|
58
|
+
canonicalEnvName: "OPENAI_API_KEY",
|
|
59
|
+
buildRequest: (key) => ({
|
|
60
|
+
url: "https://api.openai.com/v1/models",
|
|
61
|
+
headers: { Authorization: `Bearer ${key}` }
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
];
|
|
65
|
+
function createKeyHealthRoute(config) {
|
|
66
|
+
const appId = requireString("appId", config.appId);
|
|
67
|
+
const ingestSecret = requireString("ingestSecret", config.ingestSecret);
|
|
68
|
+
const fetchFn = config.fetchImpl ?? fetch;
|
|
69
|
+
const timeoutMs = config.timeoutMs ?? 3e3;
|
|
70
|
+
async function probeProvider(spec, env) {
|
|
71
|
+
let key;
|
|
72
|
+
let envName = spec.canonicalEnvName;
|
|
73
|
+
if (spec.provider === "google") {
|
|
74
|
+
const gemini = envKey(env, "GEMINI_API_KEY");
|
|
75
|
+
const google = envKey(env, "GOOGLE_API_KEY");
|
|
76
|
+
const aiSdk = envKey(env, "GOOGLE_GENERATIVE_AI_API_KEY");
|
|
77
|
+
key = gemini ?? google ?? aiSdk;
|
|
78
|
+
envName = gemini ? "GEMINI_API_KEY" : google ? "GOOGLE_API_KEY" : aiSdk ? "GOOGLE_GENERATIVE_AI_API_KEY" : "GEMINI_API_KEY";
|
|
79
|
+
} else {
|
|
80
|
+
key = envKey(env, spec.canonicalEnvName);
|
|
81
|
+
}
|
|
82
|
+
if (!key) {
|
|
83
|
+
return {
|
|
84
|
+
provider: spec.provider,
|
|
85
|
+
env: envName,
|
|
86
|
+
present: false,
|
|
87
|
+
valid: null,
|
|
88
|
+
detail: "key_absent"
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const base = {
|
|
92
|
+
provider: spec.provider,
|
|
93
|
+
env: envName,
|
|
94
|
+
present: true,
|
|
95
|
+
valid: null
|
|
96
|
+
};
|
|
97
|
+
const { url, headers } = spec.buildRequest(key);
|
|
98
|
+
const started = Date.now();
|
|
99
|
+
let res;
|
|
100
|
+
try {
|
|
101
|
+
res = await fetchFn(url, {
|
|
102
|
+
method: "GET",
|
|
103
|
+
headers,
|
|
104
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
105
|
+
});
|
|
106
|
+
} catch (err) {
|
|
107
|
+
const isTimeout = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
108
|
+
return {
|
|
109
|
+
...base,
|
|
110
|
+
latency_ms: Date.now() - started,
|
|
111
|
+
// NEVER echo err.message — provider errors could theoretically carry
|
|
112
|
+
// request context; a fixed vocabulary keeps key material impossible.
|
|
113
|
+
detail: isTimeout ? "timeout" : "network_error"
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const latencyMs = Date.now() - started;
|
|
117
|
+
if (res.ok) {
|
|
118
|
+
const result = {
|
|
119
|
+
...base,
|
|
120
|
+
valid: true,
|
|
121
|
+
status: res.status,
|
|
122
|
+
latency_ms: latencyMs
|
|
123
|
+
};
|
|
124
|
+
if (spec.parseBalanceUsd) {
|
|
125
|
+
try {
|
|
126
|
+
const body = await res.json();
|
|
127
|
+
const balance = spec.parseBalanceUsd(body);
|
|
128
|
+
if (balance !== void 0) result.balance_usd = balance;
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
if (res.status === 401 || res.status === 403) {
|
|
135
|
+
return { ...base, valid: false, status: res.status, latency_ms: latencyMs };
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
...base,
|
|
139
|
+
valid: null,
|
|
140
|
+
status: res.status,
|
|
141
|
+
latency_ms: latencyMs,
|
|
142
|
+
detail: `http_${res.status}`
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async function handle(req) {
|
|
146
|
+
try {
|
|
147
|
+
if (req.method !== "GET") {
|
|
148
|
+
return jsonResponse(405, { error: "method_not_allowed" });
|
|
149
|
+
}
|
|
150
|
+
if (bearerOf(req) !== ingestSecret) {
|
|
151
|
+
return jsonResponse(401, { error: "unauthorized" });
|
|
152
|
+
}
|
|
153
|
+
const env = config.env ?? process.env;
|
|
154
|
+
const settled = await Promise.allSettled(
|
|
155
|
+
PROBE_SPECS.map((spec) => probeProvider(spec, env))
|
|
156
|
+
);
|
|
157
|
+
const keys = settled.map((s, i) => {
|
|
158
|
+
if (s.status === "fulfilled") return s.value;
|
|
159
|
+
const spec = PROBE_SPECS[i];
|
|
160
|
+
return {
|
|
161
|
+
provider: spec.provider,
|
|
162
|
+
env: spec.canonicalEnvName,
|
|
163
|
+
present: true,
|
|
164
|
+
valid: null,
|
|
165
|
+
detail: "probe_failed"
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
keys.sort((a, b) => a.provider.localeCompare(b.provider));
|
|
169
|
+
const body = {
|
|
170
|
+
app_id: appId,
|
|
171
|
+
checked_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
172
|
+
keys
|
|
173
|
+
};
|
|
174
|
+
return jsonResponse(200, body);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
void err;
|
|
177
|
+
return jsonResponse(500, { error: "key_health_internal_error" });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { handle };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export {
|
|
184
|
+
createKeyHealthRoute
|
|
185
|
+
};
|
package/dist/index.d.mts
CHANGED
|
@@ -3,6 +3,7 @@ export { m as CallAttempt, n as CallError, o as ChainModelEntry, p as ChainWithG
|
|
|
3
3
|
import { ModelProfile, ArchetypeConvention } from './profiles.mjs';
|
|
4
4
|
export { ALIASES, CacheStrategy, CliffRule, LATENCY_TIER_MS, LatencyTier, LoweringSpec, RecoveryRule, StructuredOutputCapability, SystemPromptMode, allProfiles, getProfile, latencyTierOf, profilesByProvider, tryGetProfile } from './profiles.mjs';
|
|
5
5
|
export { BrainForwardConfig, BrainForwardRoutes, createBrainForwardRoutes } from './brain-proxy.mjs';
|
|
6
|
+
export { KeyHealthConfig, KeyHealthProvider, KeyHealthResponseBody, KeyHealthResult, KeyHealthRoute, createKeyHealthRoute } from './key-health.mjs';
|
|
6
7
|
import { IntentArchetypeName } from './dialect.mjs';
|
|
7
8
|
export { ALL_ARCHETYPES, ContextBucket, DIALECT_VERSION, HistoryDepth, INTENT_ARCHETYPES, OutputMode, ShapeSignature, ToolCountBucket, bucketContext, bucketHistory, bucketToolCount, hashShape, isArchetype, learningKey } from './dialect.mjs';
|
|
8
9
|
|
|
@@ -414,6 +415,28 @@ interface BrainConfig {
|
|
|
414
415
|
* arms the tracking, so good actors never see a false warning.
|
|
415
416
|
*/
|
|
416
417
|
warnOnDiscardedCompile?: boolean;
|
|
418
|
+
/**
|
|
419
|
+
* alpha.59 — per-leg timeout (ms) applied to every brain WRITE POST
|
|
420
|
+
* (record() primary + advisory secondary, recordOutcome(),
|
|
421
|
+
* recordShadowProbe()) via `AbortSignal.timeout`. Default 10000.
|
|
422
|
+
*
|
|
423
|
+
* Why: `sync: true` puts these fetches on the USER path (Edge consumers
|
|
424
|
+
* await record() before responding), and alpha.56's representation
|
|
425
|
+
* passthrough added a second sequential POST (advisory secondary) to that
|
|
426
|
+
* same path — an unbounded hang on either leg holds the user to the Edge
|
|
427
|
+
* limit (GE's `brainconfig-first-class-timeout` filing; GE bounded it at
|
|
428
|
+
* 3s via a hand-rolled fetchImpl wrapper every sync consumer had to
|
|
429
|
+
* rediscover).
|
|
430
|
+
*
|
|
431
|
+
* A fired timeout follows each leg's existing degraded-brain path
|
|
432
|
+
* (fire-and-forget: swallowed via onError; sync: `persistence_failed`
|
|
433
|
+
* result — never a throw into the user path). Wraps the configured
|
|
434
|
+
* `fetchImpl` rather than replacing it, so it composes with
|
|
435
|
+
* consumer-supplied impls. Set `0` to disable (pre-alpha.59 unbounded
|
|
436
|
+
* behavior). Brain-query reads (config tables) are cached + SWR and are
|
|
437
|
+
* not affected.
|
|
438
|
+
*/
|
|
439
|
+
timeoutMs?: number;
|
|
417
440
|
}
|
|
418
441
|
declare function configureBrain(config: BrainConfig): void;
|
|
419
442
|
declare function clearBrain(): void;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { m as CallAttempt, n as CallError, o as ChainModelEntry, p as ChainWithG
|
|
|
3
3
|
import { ModelProfile, ArchetypeConvention } from './profiles.js';
|
|
4
4
|
export { ALIASES, CacheStrategy, CliffRule, LATENCY_TIER_MS, LatencyTier, LoweringSpec, RecoveryRule, StructuredOutputCapability, SystemPromptMode, allProfiles, getProfile, latencyTierOf, profilesByProvider, tryGetProfile } from './profiles.js';
|
|
5
5
|
export { BrainForwardConfig, BrainForwardRoutes, createBrainForwardRoutes } from './brain-proxy.js';
|
|
6
|
+
export { KeyHealthConfig, KeyHealthProvider, KeyHealthResponseBody, KeyHealthResult, KeyHealthRoute, createKeyHealthRoute } from './key-health.js';
|
|
6
7
|
import { IntentArchetypeName } from './dialect.js';
|
|
7
8
|
export { ALL_ARCHETYPES, ContextBucket, DIALECT_VERSION, HistoryDepth, INTENT_ARCHETYPES, OutputMode, ShapeSignature, ToolCountBucket, bucketContext, bucketHistory, bucketToolCount, hashShape, isArchetype, learningKey } from './dialect.js';
|
|
8
9
|
|
|
@@ -414,6 +415,28 @@ interface BrainConfig {
|
|
|
414
415
|
* arms the tracking, so good actors never see a false warning.
|
|
415
416
|
*/
|
|
416
417
|
warnOnDiscardedCompile?: boolean;
|
|
418
|
+
/**
|
|
419
|
+
* alpha.59 — per-leg timeout (ms) applied to every brain WRITE POST
|
|
420
|
+
* (record() primary + advisory secondary, recordOutcome(),
|
|
421
|
+
* recordShadowProbe()) via `AbortSignal.timeout`. Default 10000.
|
|
422
|
+
*
|
|
423
|
+
* Why: `sync: true` puts these fetches on the USER path (Edge consumers
|
|
424
|
+
* await record() before responding), and alpha.56's representation
|
|
425
|
+
* passthrough added a second sequential POST (advisory secondary) to that
|
|
426
|
+
* same path — an unbounded hang on either leg holds the user to the Edge
|
|
427
|
+
* limit (GE's `brainconfig-first-class-timeout` filing; GE bounded it at
|
|
428
|
+
* 3s via a hand-rolled fetchImpl wrapper every sync consumer had to
|
|
429
|
+
* rediscover).
|
|
430
|
+
*
|
|
431
|
+
* A fired timeout follows each leg's existing degraded-brain path
|
|
432
|
+
* (fire-and-forget: swallowed via onError; sync: `persistence_failed`
|
|
433
|
+
* result — never a throw into the user path). Wraps the configured
|
|
434
|
+
* `fetchImpl` rather than replacing it, so it composes with
|
|
435
|
+
* consumer-supplied impls. Set `0` to disable (pre-alpha.59 unbounded
|
|
436
|
+
* behavior). Brain-query reads (config tables) are cached + SWR and are
|
|
437
|
+
* not affected.
|
|
438
|
+
*/
|
|
439
|
+
timeoutMs?: number;
|
|
417
440
|
}
|
|
418
441
|
declare function configureBrain(config: BrainConfig): void;
|
|
419
442
|
declare function clearBrain(): void;
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,7 @@ __export(index_exports, {
|
|
|
52
52
|
configureBrain: () => configureBrain,
|
|
53
53
|
countTokens: () => countTokens,
|
|
54
54
|
createBrainForwardRoutes: () => createBrainForwardRoutes,
|
|
55
|
+
createKeyHealthRoute: () => createKeyHealthRoute,
|
|
55
56
|
deriveFamilyFromModelId: () => deriveFamilyFromModelId,
|
|
56
57
|
deriveOwnership: () => deriveOwnership,
|
|
57
58
|
execute: () => execute,
|
|
@@ -4416,6 +4417,15 @@ function resolvePricingAt(modelId, at = /* @__PURE__ */ new Date()) {
|
|
|
4416
4417
|
}
|
|
4417
4418
|
|
|
4418
4419
|
// src/brain.ts
|
|
4420
|
+
var DEFAULT_BRAIN_WRITE_TIMEOUT_MS = 1e4;
|
|
4421
|
+
function brainWriteFetch(config) {
|
|
4422
|
+
const base = config.fetchImpl ?? fetch;
|
|
4423
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_BRAIN_WRITE_TIMEOUT_MS;
|
|
4424
|
+
if (timeoutMs <= 0 || typeof AbortSignal === "undefined" || typeof AbortSignal.timeout !== "function") {
|
|
4425
|
+
return base;
|
|
4426
|
+
}
|
|
4427
|
+
return (input, init) => base(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
4428
|
+
}
|
|
4419
4429
|
var activeConfig;
|
|
4420
4430
|
function configureBrain(config) {
|
|
4421
4431
|
const endpoint = config.endpoint.replace(/\/outcomes\/?$/, "");
|
|
@@ -4561,7 +4571,7 @@ async function record(input) {
|
|
|
4561
4571
|
}
|
|
4562
4572
|
const payload = buildPayload(input, reg);
|
|
4563
4573
|
const config = activeConfig;
|
|
4564
|
-
const fetchFn = config
|
|
4574
|
+
const fetchFn = brainWriteFetch(config);
|
|
4565
4575
|
const send = async () => {
|
|
4566
4576
|
let outcomeId;
|
|
4567
4577
|
try {
|
|
@@ -4730,9 +4740,9 @@ function buildAdvisoryRow(outcomeId, a) {
|
|
|
4730
4740
|
code: a.code,
|
|
4731
4741
|
level: a.level,
|
|
4732
4742
|
message: a.message,
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4743
|
+
recommendation_type: a.recommendationType ?? null,
|
|
4744
|
+
suggestion: a.suggestion ?? null,
|
|
4745
|
+
docs_url: a.docsUrl ?? null
|
|
4736
4746
|
};
|
|
4737
4747
|
}
|
|
4738
4748
|
async function recordOutcome(input) {
|
|
@@ -4740,7 +4750,7 @@ async function recordOutcome(input) {
|
|
|
4740
4750
|
return { ok: false, reason: "brain_not_configured" };
|
|
4741
4751
|
}
|
|
4742
4752
|
const config = activeConfig;
|
|
4743
|
-
const fetchFn = config
|
|
4753
|
+
const fetchFn = brainWriteFetch(config);
|
|
4744
4754
|
const payload = {
|
|
4745
4755
|
outcome_id: input.outcomeId,
|
|
4746
4756
|
outcome: input.outcome,
|
|
@@ -4815,7 +4825,7 @@ function buildShadowProbeRow(input) {
|
|
|
4815
4825
|
async function recordShadowProbe(input) {
|
|
4816
4826
|
if (!activeConfig) return;
|
|
4817
4827
|
const config = activeConfig;
|
|
4818
|
-
const fetchFn = config
|
|
4828
|
+
const fetchFn = brainWriteFetch(config);
|
|
4819
4829
|
const row = buildShadowProbeRow(input);
|
|
4820
4830
|
const send = async () => {
|
|
4821
4831
|
try {
|
|
@@ -6935,6 +6945,188 @@ function createBrainForwardRoutes(config) {
|
|
|
6935
6945
|
return { handle, segments: KNOWN_SEGMENTS };
|
|
6936
6946
|
}
|
|
6937
6947
|
|
|
6948
|
+
// src/key-health.ts
|
|
6949
|
+
var JSON_HEADERS2 = { "Content-Type": "application/json" };
|
|
6950
|
+
function jsonResponse2(status, body) {
|
|
6951
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS2 });
|
|
6952
|
+
}
|
|
6953
|
+
function bearerOf2(req) {
|
|
6954
|
+
const header = req.headers.get("Authorization") ?? "";
|
|
6955
|
+
const match = /^Bearer\s+(.+)$/i.exec(header);
|
|
6956
|
+
return match?.[1]?.trim() ?? "";
|
|
6957
|
+
}
|
|
6958
|
+
function requireString2(name, value) {
|
|
6959
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
6960
|
+
throw new Error(`createKeyHealthRoute: ${name} is required`);
|
|
6961
|
+
}
|
|
6962
|
+
return value;
|
|
6963
|
+
}
|
|
6964
|
+
function envKey(env, name) {
|
|
6965
|
+
const v = env[name];
|
|
6966
|
+
if (typeof v !== "string") return void 0;
|
|
6967
|
+
const trimmed = v.trim();
|
|
6968
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
6969
|
+
}
|
|
6970
|
+
var PROBE_SPECS = [
|
|
6971
|
+
{
|
|
6972
|
+
provider: "anthropic",
|
|
6973
|
+
canonicalEnvName: "ANTHROPIC_API_KEY",
|
|
6974
|
+
buildRequest: (key) => ({
|
|
6975
|
+
url: "https://api.anthropic.com/v1/models?limit=1",
|
|
6976
|
+
headers: { "x-api-key": key, "anthropic-version": "2023-06-01" }
|
|
6977
|
+
})
|
|
6978
|
+
},
|
|
6979
|
+
{
|
|
6980
|
+
provider: "deepseek",
|
|
6981
|
+
canonicalEnvName: "DEEPSEEK_API_KEY",
|
|
6982
|
+
buildRequest: (key) => ({
|
|
6983
|
+
url: "https://api.deepseek.com/user/balance",
|
|
6984
|
+
headers: { Authorization: `Bearer ${key}` }
|
|
6985
|
+
}),
|
|
6986
|
+
parseBalanceUsd: (body) => {
|
|
6987
|
+
const infos = body?.balance_infos;
|
|
6988
|
+
if (!Array.isArray(infos) || infos.length === 0) return void 0;
|
|
6989
|
+
const first = infos[0];
|
|
6990
|
+
if (first?.currency !== "USD") return void 0;
|
|
6991
|
+
const n = Number(first.total_balance);
|
|
6992
|
+
return Number.isFinite(n) ? n : void 0;
|
|
6993
|
+
}
|
|
6994
|
+
},
|
|
6995
|
+
{
|
|
6996
|
+
provider: "google",
|
|
6997
|
+
canonicalEnvName: "GEMINI_API_KEY",
|
|
6998
|
+
buildRequest: (key) => ({
|
|
6999
|
+
url: `https://generativelanguage.googleapis.com/v1beta/models?pageSize=1&key=${encodeURIComponent(key)}`,
|
|
7000
|
+
headers: {}
|
|
7001
|
+
})
|
|
7002
|
+
},
|
|
7003
|
+
{
|
|
7004
|
+
provider: "openai",
|
|
7005
|
+
canonicalEnvName: "OPENAI_API_KEY",
|
|
7006
|
+
buildRequest: (key) => ({
|
|
7007
|
+
url: "https://api.openai.com/v1/models",
|
|
7008
|
+
headers: { Authorization: `Bearer ${key}` }
|
|
7009
|
+
})
|
|
7010
|
+
}
|
|
7011
|
+
];
|
|
7012
|
+
function createKeyHealthRoute(config) {
|
|
7013
|
+
const appId = requireString2("appId", config.appId);
|
|
7014
|
+
const ingestSecret = requireString2("ingestSecret", config.ingestSecret);
|
|
7015
|
+
const fetchFn = config.fetchImpl ?? fetch;
|
|
7016
|
+
const timeoutMs = config.timeoutMs ?? 3e3;
|
|
7017
|
+
async function probeProvider(spec, env) {
|
|
7018
|
+
let key;
|
|
7019
|
+
let envName = spec.canonicalEnvName;
|
|
7020
|
+
if (spec.provider === "google") {
|
|
7021
|
+
const gemini = envKey(env, "GEMINI_API_KEY");
|
|
7022
|
+
const google = envKey(env, "GOOGLE_API_KEY");
|
|
7023
|
+
const aiSdk = envKey(env, "GOOGLE_GENERATIVE_AI_API_KEY");
|
|
7024
|
+
key = gemini ?? google ?? aiSdk;
|
|
7025
|
+
envName = gemini ? "GEMINI_API_KEY" : google ? "GOOGLE_API_KEY" : aiSdk ? "GOOGLE_GENERATIVE_AI_API_KEY" : "GEMINI_API_KEY";
|
|
7026
|
+
} else {
|
|
7027
|
+
key = envKey(env, spec.canonicalEnvName);
|
|
7028
|
+
}
|
|
7029
|
+
if (!key) {
|
|
7030
|
+
return {
|
|
7031
|
+
provider: spec.provider,
|
|
7032
|
+
env: envName,
|
|
7033
|
+
present: false,
|
|
7034
|
+
valid: null,
|
|
7035
|
+
detail: "key_absent"
|
|
7036
|
+
};
|
|
7037
|
+
}
|
|
7038
|
+
const base = {
|
|
7039
|
+
provider: spec.provider,
|
|
7040
|
+
env: envName,
|
|
7041
|
+
present: true,
|
|
7042
|
+
valid: null
|
|
7043
|
+
};
|
|
7044
|
+
const { url, headers } = spec.buildRequest(key);
|
|
7045
|
+
const started = Date.now();
|
|
7046
|
+
let res;
|
|
7047
|
+
try {
|
|
7048
|
+
res = await fetchFn(url, {
|
|
7049
|
+
method: "GET",
|
|
7050
|
+
headers,
|
|
7051
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
7052
|
+
});
|
|
7053
|
+
} catch (err) {
|
|
7054
|
+
const isTimeout = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
7055
|
+
return {
|
|
7056
|
+
...base,
|
|
7057
|
+
latency_ms: Date.now() - started,
|
|
7058
|
+
// NEVER echo err.message — provider errors could theoretically carry
|
|
7059
|
+
// request context; a fixed vocabulary keeps key material impossible.
|
|
7060
|
+
detail: isTimeout ? "timeout" : "network_error"
|
|
7061
|
+
};
|
|
7062
|
+
}
|
|
7063
|
+
const latencyMs = Date.now() - started;
|
|
7064
|
+
if (res.ok) {
|
|
7065
|
+
const result = {
|
|
7066
|
+
...base,
|
|
7067
|
+
valid: true,
|
|
7068
|
+
status: res.status,
|
|
7069
|
+
latency_ms: latencyMs
|
|
7070
|
+
};
|
|
7071
|
+
if (spec.parseBalanceUsd) {
|
|
7072
|
+
try {
|
|
7073
|
+
const body = await res.json();
|
|
7074
|
+
const balance = spec.parseBalanceUsd(body);
|
|
7075
|
+
if (balance !== void 0) result.balance_usd = balance;
|
|
7076
|
+
} catch {
|
|
7077
|
+
}
|
|
7078
|
+
}
|
|
7079
|
+
return result;
|
|
7080
|
+
}
|
|
7081
|
+
if (res.status === 401 || res.status === 403) {
|
|
7082
|
+
return { ...base, valid: false, status: res.status, latency_ms: latencyMs };
|
|
7083
|
+
}
|
|
7084
|
+
return {
|
|
7085
|
+
...base,
|
|
7086
|
+
valid: null,
|
|
7087
|
+
status: res.status,
|
|
7088
|
+
latency_ms: latencyMs,
|
|
7089
|
+
detail: `http_${res.status}`
|
|
7090
|
+
};
|
|
7091
|
+
}
|
|
7092
|
+
async function handle(req) {
|
|
7093
|
+
try {
|
|
7094
|
+
if (req.method !== "GET") {
|
|
7095
|
+
return jsonResponse2(405, { error: "method_not_allowed" });
|
|
7096
|
+
}
|
|
7097
|
+
if (bearerOf2(req) !== ingestSecret) {
|
|
7098
|
+
return jsonResponse2(401, { error: "unauthorized" });
|
|
7099
|
+
}
|
|
7100
|
+
const env = config.env ?? process.env;
|
|
7101
|
+
const settled = await Promise.allSettled(
|
|
7102
|
+
PROBE_SPECS.map((spec) => probeProvider(spec, env))
|
|
7103
|
+
);
|
|
7104
|
+
const keys = settled.map((s, i) => {
|
|
7105
|
+
if (s.status === "fulfilled") return s.value;
|
|
7106
|
+
const spec = PROBE_SPECS[i];
|
|
7107
|
+
return {
|
|
7108
|
+
provider: spec.provider,
|
|
7109
|
+
env: spec.canonicalEnvName,
|
|
7110
|
+
present: true,
|
|
7111
|
+
valid: null,
|
|
7112
|
+
detail: "probe_failed"
|
|
7113
|
+
};
|
|
7114
|
+
});
|
|
7115
|
+
keys.sort((a, b) => a.provider.localeCompare(b.provider));
|
|
7116
|
+
const body = {
|
|
7117
|
+
app_id: appId,
|
|
7118
|
+
checked_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7119
|
+
keys
|
|
7120
|
+
};
|
|
7121
|
+
return jsonResponse2(200, body);
|
|
7122
|
+
} catch (err) {
|
|
7123
|
+
void err;
|
|
7124
|
+
return jsonResponse2(500, { error: "key_health_internal_error" });
|
|
7125
|
+
}
|
|
7126
|
+
}
|
|
7127
|
+
return { handle };
|
|
7128
|
+
}
|
|
7129
|
+
|
|
6938
7130
|
// src/oracle.ts
|
|
6939
7131
|
var DEFAULT_DIMENSIONS = ["correctness", "completeness", "conciseness", "format"];
|
|
6940
7132
|
var judgeCallTimes = [];
|
|
@@ -7395,6 +7587,7 @@ function compile2(ir, opts) {
|
|
|
7395
7587
|
configureBrain,
|
|
7396
7588
|
countTokens,
|
|
7397
7589
|
createBrainForwardRoutes,
|
|
7590
|
+
createKeyHealthRoute,
|
|
7398
7591
|
deriveFamilyFromModelId,
|
|
7399
7592
|
deriveOwnership,
|
|
7400
7593
|
execute,
|
package/dist/index.mjs
CHANGED
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
isArchetype,
|
|
13
13
|
learningKey
|
|
14
14
|
} from "./chunk-5TI6PNSK.mjs";
|
|
15
|
+
import {
|
|
16
|
+
createKeyHealthRoute
|
|
17
|
+
} from "./chunk-B4INZ32V.mjs";
|
|
15
18
|
import {
|
|
16
19
|
ABSOLUTE_FLOOR,
|
|
17
20
|
ARCHETYPE_FLOOR_DEFAULT,
|
|
@@ -2572,6 +2575,15 @@ function resolvePricingAt(modelId, at = /* @__PURE__ */ new Date()) {
|
|
|
2572
2575
|
}
|
|
2573
2576
|
|
|
2574
2577
|
// src/brain.ts
|
|
2578
|
+
var DEFAULT_BRAIN_WRITE_TIMEOUT_MS = 1e4;
|
|
2579
|
+
function brainWriteFetch(config) {
|
|
2580
|
+
const base = config.fetchImpl ?? fetch;
|
|
2581
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_BRAIN_WRITE_TIMEOUT_MS;
|
|
2582
|
+
if (timeoutMs <= 0 || typeof AbortSignal === "undefined" || typeof AbortSignal.timeout !== "function") {
|
|
2583
|
+
return base;
|
|
2584
|
+
}
|
|
2585
|
+
return (input, init) => base(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
2586
|
+
}
|
|
2575
2587
|
var activeConfig;
|
|
2576
2588
|
function configureBrain(config) {
|
|
2577
2589
|
const endpoint = config.endpoint.replace(/\/outcomes\/?$/, "");
|
|
@@ -2717,7 +2729,7 @@ async function record(input) {
|
|
|
2717
2729
|
}
|
|
2718
2730
|
const payload = buildPayload(input, reg);
|
|
2719
2731
|
const config = activeConfig;
|
|
2720
|
-
const fetchFn = config
|
|
2732
|
+
const fetchFn = brainWriteFetch(config);
|
|
2721
2733
|
const send = async () => {
|
|
2722
2734
|
let outcomeId;
|
|
2723
2735
|
try {
|
|
@@ -2886,9 +2898,9 @@ function buildAdvisoryRow(outcomeId, a) {
|
|
|
2886
2898
|
code: a.code,
|
|
2887
2899
|
level: a.level,
|
|
2888
2900
|
message: a.message,
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2901
|
+
recommendation_type: a.recommendationType ?? null,
|
|
2902
|
+
suggestion: a.suggestion ?? null,
|
|
2903
|
+
docs_url: a.docsUrl ?? null
|
|
2892
2904
|
};
|
|
2893
2905
|
}
|
|
2894
2906
|
async function recordOutcome(input) {
|
|
@@ -2896,7 +2908,7 @@ async function recordOutcome(input) {
|
|
|
2896
2908
|
return { ok: false, reason: "brain_not_configured" };
|
|
2897
2909
|
}
|
|
2898
2910
|
const config = activeConfig;
|
|
2899
|
-
const fetchFn = config
|
|
2911
|
+
const fetchFn = brainWriteFetch(config);
|
|
2900
2912
|
const payload = {
|
|
2901
2913
|
outcome_id: input.outcomeId,
|
|
2902
2914
|
outcome: input.outcome,
|
|
@@ -2971,7 +2983,7 @@ function buildShadowProbeRow(input) {
|
|
|
2971
2983
|
async function recordShadowProbe(input) {
|
|
2972
2984
|
if (!activeConfig) return;
|
|
2973
2985
|
const config = activeConfig;
|
|
2974
|
-
const fetchFn = config
|
|
2986
|
+
const fetchFn = brainWriteFetch(config);
|
|
2975
2987
|
const row = buildShadowProbeRow(input);
|
|
2976
2988
|
const send = async () => {
|
|
2977
2989
|
try {
|
|
@@ -4673,6 +4685,7 @@ export {
|
|
|
4673
4685
|
configureBrain,
|
|
4674
4686
|
countTokens,
|
|
4675
4687
|
createBrainForwardRoutes,
|
|
4688
|
+
createKeyHealthRoute,
|
|
4676
4689
|
deriveFamilyFromModelId,
|
|
4677
4690
|
deriveOwnership,
|
|
4678
4691
|
execute,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@warmdrift/kgauto-compiler/key-health` — per-consumer provider-key health
|
|
3
|
+
* endpoint factory (alpha.58, KG-fleet-v1 phase 1).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* Consumers (playbacksam, inspire-central, tt-intelligence, global-expansion)
|
|
8
|
+
* hold their own provider API keys in their own Vercel envs — the library
|
|
9
|
+
* never sees keys centrally, by design. That means the operator dashboard has
|
|
10
|
+
* no way to answer "is PB's DeepSeek key still valid?" without a per-consumer
|
|
11
|
+
* probe surface. This factory is that surface: the consumer mounts ONE route,
|
|
12
|
+
* the dashboard polls it with the consumer's existing `KGAUTO_INGEST_SECRET`
|
|
13
|
+
* bearer, and gets back a validity grid for every canonical provider key.
|
|
14
|
+
*
|
|
15
|
+
* Probes are the cheapest authenticated GETs each provider offers (model
|
|
16
|
+
* listings; DeepSeek's balance endpoint) — never a completion call, zero
|
|
17
|
+
* token cost. Keys themselves NEVER appear in any response: only presence,
|
|
18
|
+
* validity, HTTP status, latency, and (DeepSeek only) account balance.
|
|
19
|
+
*
|
|
20
|
+
* Same design contract as `createBrainForwardRoutes` (brain-proxy.ts):
|
|
21
|
+
* web-standard Request→Response, never throws (every path returns a
|
|
22
|
+
* `Response`), bearer gate, runs on Edge/Workers/Node.
|
|
23
|
+
*
|
|
24
|
+
* ## Mounting recipes
|
|
25
|
+
*
|
|
26
|
+
* ### (a) Next.js app-router route handler — ONE file
|
|
27
|
+
*
|
|
28
|
+
* // app/api/kgauto/v2/keyhealth/route.ts
|
|
29
|
+
* import { createKeyHealthRoute } from '@warmdrift/kgauto-compiler/key-health';
|
|
30
|
+
* const routes = createKeyHealthRoute({
|
|
31
|
+
* appId: 'playbacksam',
|
|
32
|
+
* ingestSecret: process.env.KGAUTO_INGEST_SECRET!,
|
|
33
|
+
* });
|
|
34
|
+
* export const GET = (req: Request) => routes.handle(req);
|
|
35
|
+
*
|
|
36
|
+
* // middleware.ts — if the app gates routes behind auth middleware, the
|
|
37
|
+
* // existing brain-proxy public-path prefix ('/api/kgauto/v2/') already
|
|
38
|
+
* // covers this route. If the allowlist is per-path, add
|
|
39
|
+
* // '/api/kgauto/v2/keyhealth' or server-to-server GETs 307 to /login
|
|
40
|
+
* // (the tt-intel middleware-drift shape — see brain-proxy.ts).
|
|
41
|
+
*
|
|
42
|
+
* ### (b) Plain per-file Vercel function
|
|
43
|
+
*
|
|
44
|
+
* // api/kgauto/keyhealth.ts
|
|
45
|
+
* export const config = { runtime: 'edge' };
|
|
46
|
+
* import { createKeyHealthRoute } from '@warmdrift/kgauto-compiler/key-health';
|
|
47
|
+
* const routes = createKeyHealthRoute({
|
|
48
|
+
* appId: 'inspire-central',
|
|
49
|
+
* ingestSecret: process.env.KGAUTO_INGEST_SECRET!,
|
|
50
|
+
* });
|
|
51
|
+
* export default (req: Request) => routes.handle(req);
|
|
52
|
+
*
|
|
53
|
+
* ## Wire shape (LOCKED CONTRACT with the operator dashboard — do not rename)
|
|
54
|
+
*
|
|
55
|
+
* 200 {
|
|
56
|
+
* app_id: string,
|
|
57
|
+
* checked_at: string, // ISO timestamp
|
|
58
|
+
* keys: [ // sorted by provider name
|
|
59
|
+
* {
|
|
60
|
+
* provider: 'anthropic' | 'deepseek' | 'google' | 'openai',
|
|
61
|
+
* env: string, // canonical env var name probed
|
|
62
|
+
* present: boolean, // key present (non-empty after trim)
|
|
63
|
+
* valid: boolean | null, // true 2xx / false 401·403 / null unknown
|
|
64
|
+
* status?: number, // probe HTTP status when a response landed
|
|
65
|
+
* latency_ms?: number, // probe round-trip
|
|
66
|
+
* balance_usd?: number, // DeepSeek only, when currency is USD
|
|
67
|
+
* detail?: string, // e.g. 'timeout', 'http_500', 'key_absent'
|
|
68
|
+
* }
|
|
69
|
+
* ]
|
|
70
|
+
* }
|
|
71
|
+
*/
|
|
72
|
+
type KeyHealthProvider = 'anthropic' | 'deepseek' | 'google' | 'openai';
|
|
73
|
+
interface KeyHealthConfig {
|
|
74
|
+
/** Consumer app id echoed as `app_id` in the response (e.g. 'playbacksam'). */
|
|
75
|
+
appId: string;
|
|
76
|
+
/** Bearer token the poller must present (consumer's existing KGAUTO_INGEST_SECRET). */
|
|
77
|
+
ingestSecret: string;
|
|
78
|
+
/** Optional fetch impl for tests. */
|
|
79
|
+
fetchImpl?: typeof fetch;
|
|
80
|
+
/** Env source; defaults to process.env. Test injection point. */
|
|
81
|
+
env?: Record<string, string | undefined>;
|
|
82
|
+
/** Per-provider probe timeout in ms. Default 3000. */
|
|
83
|
+
timeoutMs?: number;
|
|
84
|
+
}
|
|
85
|
+
interface KeyHealthResult {
|
|
86
|
+
provider: KeyHealthProvider;
|
|
87
|
+
/** Canonical env var name probed (the resolved one for Google). */
|
|
88
|
+
env: string;
|
|
89
|
+
/** Key present (non-empty after trim) in env. */
|
|
90
|
+
present: boolean;
|
|
91
|
+
/** true = provider accepted the key (2xx); false = rejected (401/403); null = unknown (absent / timeout / 5xx / 429 / network). */
|
|
92
|
+
valid: boolean | null;
|
|
93
|
+
/** HTTP status of the probe, when a response landed. */
|
|
94
|
+
status?: number;
|
|
95
|
+
/** Probe round-trip in ms, when a probe ran. */
|
|
96
|
+
latency_ms?: number;
|
|
97
|
+
/** DeepSeek only: account balance in USD when the balance API reports USD. */
|
|
98
|
+
balance_usd?: number;
|
|
99
|
+
/** Failure class when valid is null with a probe attempted (e.g. 'timeout', 'http_500') or 'key_absent'. */
|
|
100
|
+
detail?: string;
|
|
101
|
+
}
|
|
102
|
+
interface KeyHealthResponseBody {
|
|
103
|
+
app_id: string;
|
|
104
|
+
checked_at: string;
|
|
105
|
+
keys: KeyHealthResult[];
|
|
106
|
+
}
|
|
107
|
+
interface KeyHealthRoute {
|
|
108
|
+
/**
|
|
109
|
+
* Web-standard handler. GET only. Never throws — every error path returns
|
|
110
|
+
* a `Response`.
|
|
111
|
+
*/
|
|
112
|
+
handle(req: Request): Promise<Response>;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Create the per-consumer key-health route. See module docstring for the
|
|
116
|
+
* mounting recipes ((a) Next.js route handler, (b) per-file Vercel function)
|
|
117
|
+
* and the locked wire shape.
|
|
118
|
+
*/
|
|
119
|
+
declare function createKeyHealthRoute(config: KeyHealthConfig): KeyHealthRoute;
|
|
120
|
+
|
|
121
|
+
export { type KeyHealthConfig, type KeyHealthProvider, type KeyHealthResponseBody, type KeyHealthResult, type KeyHealthRoute, createKeyHealthRoute };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@warmdrift/kgauto-compiler/key-health` — per-consumer provider-key health
|
|
3
|
+
* endpoint factory (alpha.58, KG-fleet-v1 phase 1).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* Consumers (playbacksam, inspire-central, tt-intelligence, global-expansion)
|
|
8
|
+
* hold their own provider API keys in their own Vercel envs — the library
|
|
9
|
+
* never sees keys centrally, by design. That means the operator dashboard has
|
|
10
|
+
* no way to answer "is PB's DeepSeek key still valid?" without a per-consumer
|
|
11
|
+
* probe surface. This factory is that surface: the consumer mounts ONE route,
|
|
12
|
+
* the dashboard polls it with the consumer's existing `KGAUTO_INGEST_SECRET`
|
|
13
|
+
* bearer, and gets back a validity grid for every canonical provider key.
|
|
14
|
+
*
|
|
15
|
+
* Probes are the cheapest authenticated GETs each provider offers (model
|
|
16
|
+
* listings; DeepSeek's balance endpoint) — never a completion call, zero
|
|
17
|
+
* token cost. Keys themselves NEVER appear in any response: only presence,
|
|
18
|
+
* validity, HTTP status, latency, and (DeepSeek only) account balance.
|
|
19
|
+
*
|
|
20
|
+
* Same design contract as `createBrainForwardRoutes` (brain-proxy.ts):
|
|
21
|
+
* web-standard Request→Response, never throws (every path returns a
|
|
22
|
+
* `Response`), bearer gate, runs on Edge/Workers/Node.
|
|
23
|
+
*
|
|
24
|
+
* ## Mounting recipes
|
|
25
|
+
*
|
|
26
|
+
* ### (a) Next.js app-router route handler — ONE file
|
|
27
|
+
*
|
|
28
|
+
* // app/api/kgauto/v2/keyhealth/route.ts
|
|
29
|
+
* import { createKeyHealthRoute } from '@warmdrift/kgauto-compiler/key-health';
|
|
30
|
+
* const routes = createKeyHealthRoute({
|
|
31
|
+
* appId: 'playbacksam',
|
|
32
|
+
* ingestSecret: process.env.KGAUTO_INGEST_SECRET!,
|
|
33
|
+
* });
|
|
34
|
+
* export const GET = (req: Request) => routes.handle(req);
|
|
35
|
+
*
|
|
36
|
+
* // middleware.ts — if the app gates routes behind auth middleware, the
|
|
37
|
+
* // existing brain-proxy public-path prefix ('/api/kgauto/v2/') already
|
|
38
|
+
* // covers this route. If the allowlist is per-path, add
|
|
39
|
+
* // '/api/kgauto/v2/keyhealth' or server-to-server GETs 307 to /login
|
|
40
|
+
* // (the tt-intel middleware-drift shape — see brain-proxy.ts).
|
|
41
|
+
*
|
|
42
|
+
* ### (b) Plain per-file Vercel function
|
|
43
|
+
*
|
|
44
|
+
* // api/kgauto/keyhealth.ts
|
|
45
|
+
* export const config = { runtime: 'edge' };
|
|
46
|
+
* import { createKeyHealthRoute } from '@warmdrift/kgauto-compiler/key-health';
|
|
47
|
+
* const routes = createKeyHealthRoute({
|
|
48
|
+
* appId: 'inspire-central',
|
|
49
|
+
* ingestSecret: process.env.KGAUTO_INGEST_SECRET!,
|
|
50
|
+
* });
|
|
51
|
+
* export default (req: Request) => routes.handle(req);
|
|
52
|
+
*
|
|
53
|
+
* ## Wire shape (LOCKED CONTRACT with the operator dashboard — do not rename)
|
|
54
|
+
*
|
|
55
|
+
* 200 {
|
|
56
|
+
* app_id: string,
|
|
57
|
+
* checked_at: string, // ISO timestamp
|
|
58
|
+
* keys: [ // sorted by provider name
|
|
59
|
+
* {
|
|
60
|
+
* provider: 'anthropic' | 'deepseek' | 'google' | 'openai',
|
|
61
|
+
* env: string, // canonical env var name probed
|
|
62
|
+
* present: boolean, // key present (non-empty after trim)
|
|
63
|
+
* valid: boolean | null, // true 2xx / false 401·403 / null unknown
|
|
64
|
+
* status?: number, // probe HTTP status when a response landed
|
|
65
|
+
* latency_ms?: number, // probe round-trip
|
|
66
|
+
* balance_usd?: number, // DeepSeek only, when currency is USD
|
|
67
|
+
* detail?: string, // e.g. 'timeout', 'http_500', 'key_absent'
|
|
68
|
+
* }
|
|
69
|
+
* ]
|
|
70
|
+
* }
|
|
71
|
+
*/
|
|
72
|
+
type KeyHealthProvider = 'anthropic' | 'deepseek' | 'google' | 'openai';
|
|
73
|
+
interface KeyHealthConfig {
|
|
74
|
+
/** Consumer app id echoed as `app_id` in the response (e.g. 'playbacksam'). */
|
|
75
|
+
appId: string;
|
|
76
|
+
/** Bearer token the poller must present (consumer's existing KGAUTO_INGEST_SECRET). */
|
|
77
|
+
ingestSecret: string;
|
|
78
|
+
/** Optional fetch impl for tests. */
|
|
79
|
+
fetchImpl?: typeof fetch;
|
|
80
|
+
/** Env source; defaults to process.env. Test injection point. */
|
|
81
|
+
env?: Record<string, string | undefined>;
|
|
82
|
+
/** Per-provider probe timeout in ms. Default 3000. */
|
|
83
|
+
timeoutMs?: number;
|
|
84
|
+
}
|
|
85
|
+
interface KeyHealthResult {
|
|
86
|
+
provider: KeyHealthProvider;
|
|
87
|
+
/** Canonical env var name probed (the resolved one for Google). */
|
|
88
|
+
env: string;
|
|
89
|
+
/** Key present (non-empty after trim) in env. */
|
|
90
|
+
present: boolean;
|
|
91
|
+
/** true = provider accepted the key (2xx); false = rejected (401/403); null = unknown (absent / timeout / 5xx / 429 / network). */
|
|
92
|
+
valid: boolean | null;
|
|
93
|
+
/** HTTP status of the probe, when a response landed. */
|
|
94
|
+
status?: number;
|
|
95
|
+
/** Probe round-trip in ms, when a probe ran. */
|
|
96
|
+
latency_ms?: number;
|
|
97
|
+
/** DeepSeek only: account balance in USD when the balance API reports USD. */
|
|
98
|
+
balance_usd?: number;
|
|
99
|
+
/** Failure class when valid is null with a probe attempted (e.g. 'timeout', 'http_500') or 'key_absent'. */
|
|
100
|
+
detail?: string;
|
|
101
|
+
}
|
|
102
|
+
interface KeyHealthResponseBody {
|
|
103
|
+
app_id: string;
|
|
104
|
+
checked_at: string;
|
|
105
|
+
keys: KeyHealthResult[];
|
|
106
|
+
}
|
|
107
|
+
interface KeyHealthRoute {
|
|
108
|
+
/**
|
|
109
|
+
* Web-standard handler. GET only. Never throws — every error path returns
|
|
110
|
+
* a `Response`.
|
|
111
|
+
*/
|
|
112
|
+
handle(req: Request): Promise<Response>;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Create the per-consumer key-health route. See module docstring for the
|
|
116
|
+
* mounting recipes ((a) Next.js route handler, (b) per-file Vercel function)
|
|
117
|
+
* and the locked wire shape.
|
|
118
|
+
*/
|
|
119
|
+
declare function createKeyHealthRoute(config: KeyHealthConfig): KeyHealthRoute;
|
|
120
|
+
|
|
121
|
+
export { type KeyHealthConfig, type KeyHealthProvider, type KeyHealthResponseBody, type KeyHealthResult, type KeyHealthRoute, createKeyHealthRoute };
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/key-health.ts
|
|
21
|
+
var key_health_exports = {};
|
|
22
|
+
__export(key_health_exports, {
|
|
23
|
+
createKeyHealthRoute: () => createKeyHealthRoute
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(key_health_exports);
|
|
26
|
+
var JSON_HEADERS = { "Content-Type": "application/json" };
|
|
27
|
+
function jsonResponse(status, body) {
|
|
28
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
|
|
29
|
+
}
|
|
30
|
+
function bearerOf(req) {
|
|
31
|
+
const header = req.headers.get("Authorization") ?? "";
|
|
32
|
+
const match = /^Bearer\s+(.+)$/i.exec(header);
|
|
33
|
+
return match?.[1]?.trim() ?? "";
|
|
34
|
+
}
|
|
35
|
+
function requireString(name, value) {
|
|
36
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
37
|
+
throw new Error(`createKeyHealthRoute: ${name} is required`);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function envKey(env, name) {
|
|
42
|
+
const v = env[name];
|
|
43
|
+
if (typeof v !== "string") return void 0;
|
|
44
|
+
const trimmed = v.trim();
|
|
45
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
46
|
+
}
|
|
47
|
+
var PROBE_SPECS = [
|
|
48
|
+
{
|
|
49
|
+
provider: "anthropic",
|
|
50
|
+
canonicalEnvName: "ANTHROPIC_API_KEY",
|
|
51
|
+
buildRequest: (key) => ({
|
|
52
|
+
url: "https://api.anthropic.com/v1/models?limit=1",
|
|
53
|
+
headers: { "x-api-key": key, "anthropic-version": "2023-06-01" }
|
|
54
|
+
})
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
provider: "deepseek",
|
|
58
|
+
canonicalEnvName: "DEEPSEEK_API_KEY",
|
|
59
|
+
buildRequest: (key) => ({
|
|
60
|
+
url: "https://api.deepseek.com/user/balance",
|
|
61
|
+
headers: { Authorization: `Bearer ${key}` }
|
|
62
|
+
}),
|
|
63
|
+
parseBalanceUsd: (body) => {
|
|
64
|
+
const infos = body?.balance_infos;
|
|
65
|
+
if (!Array.isArray(infos) || infos.length === 0) return void 0;
|
|
66
|
+
const first = infos[0];
|
|
67
|
+
if (first?.currency !== "USD") return void 0;
|
|
68
|
+
const n = Number(first.total_balance);
|
|
69
|
+
return Number.isFinite(n) ? n : void 0;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
provider: "google",
|
|
74
|
+
canonicalEnvName: "GEMINI_API_KEY",
|
|
75
|
+
buildRequest: (key) => ({
|
|
76
|
+
url: `https://generativelanguage.googleapis.com/v1beta/models?pageSize=1&key=${encodeURIComponent(key)}`,
|
|
77
|
+
headers: {}
|
|
78
|
+
})
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
provider: "openai",
|
|
82
|
+
canonicalEnvName: "OPENAI_API_KEY",
|
|
83
|
+
buildRequest: (key) => ({
|
|
84
|
+
url: "https://api.openai.com/v1/models",
|
|
85
|
+
headers: { Authorization: `Bearer ${key}` }
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
function createKeyHealthRoute(config) {
|
|
90
|
+
const appId = requireString("appId", config.appId);
|
|
91
|
+
const ingestSecret = requireString("ingestSecret", config.ingestSecret);
|
|
92
|
+
const fetchFn = config.fetchImpl ?? fetch;
|
|
93
|
+
const timeoutMs = config.timeoutMs ?? 3e3;
|
|
94
|
+
async function probeProvider(spec, env) {
|
|
95
|
+
let key;
|
|
96
|
+
let envName = spec.canonicalEnvName;
|
|
97
|
+
if (spec.provider === "google") {
|
|
98
|
+
const gemini = envKey(env, "GEMINI_API_KEY");
|
|
99
|
+
const google = envKey(env, "GOOGLE_API_KEY");
|
|
100
|
+
const aiSdk = envKey(env, "GOOGLE_GENERATIVE_AI_API_KEY");
|
|
101
|
+
key = gemini ?? google ?? aiSdk;
|
|
102
|
+
envName = gemini ? "GEMINI_API_KEY" : google ? "GOOGLE_API_KEY" : aiSdk ? "GOOGLE_GENERATIVE_AI_API_KEY" : "GEMINI_API_KEY";
|
|
103
|
+
} else {
|
|
104
|
+
key = envKey(env, spec.canonicalEnvName);
|
|
105
|
+
}
|
|
106
|
+
if (!key) {
|
|
107
|
+
return {
|
|
108
|
+
provider: spec.provider,
|
|
109
|
+
env: envName,
|
|
110
|
+
present: false,
|
|
111
|
+
valid: null,
|
|
112
|
+
detail: "key_absent"
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const base = {
|
|
116
|
+
provider: spec.provider,
|
|
117
|
+
env: envName,
|
|
118
|
+
present: true,
|
|
119
|
+
valid: null
|
|
120
|
+
};
|
|
121
|
+
const { url, headers } = spec.buildRequest(key);
|
|
122
|
+
const started = Date.now();
|
|
123
|
+
let res;
|
|
124
|
+
try {
|
|
125
|
+
res = await fetchFn(url, {
|
|
126
|
+
method: "GET",
|
|
127
|
+
headers,
|
|
128
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
129
|
+
});
|
|
130
|
+
} catch (err) {
|
|
131
|
+
const isTimeout = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
132
|
+
return {
|
|
133
|
+
...base,
|
|
134
|
+
latency_ms: Date.now() - started,
|
|
135
|
+
// NEVER echo err.message — provider errors could theoretically carry
|
|
136
|
+
// request context; a fixed vocabulary keeps key material impossible.
|
|
137
|
+
detail: isTimeout ? "timeout" : "network_error"
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const latencyMs = Date.now() - started;
|
|
141
|
+
if (res.ok) {
|
|
142
|
+
const result = {
|
|
143
|
+
...base,
|
|
144
|
+
valid: true,
|
|
145
|
+
status: res.status,
|
|
146
|
+
latency_ms: latencyMs
|
|
147
|
+
};
|
|
148
|
+
if (spec.parseBalanceUsd) {
|
|
149
|
+
try {
|
|
150
|
+
const body = await res.json();
|
|
151
|
+
const balance = spec.parseBalanceUsd(body);
|
|
152
|
+
if (balance !== void 0) result.balance_usd = balance;
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
if (res.status === 401 || res.status === 403) {
|
|
159
|
+
return { ...base, valid: false, status: res.status, latency_ms: latencyMs };
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
...base,
|
|
163
|
+
valid: null,
|
|
164
|
+
status: res.status,
|
|
165
|
+
latency_ms: latencyMs,
|
|
166
|
+
detail: `http_${res.status}`
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async function handle(req) {
|
|
170
|
+
try {
|
|
171
|
+
if (req.method !== "GET") {
|
|
172
|
+
return jsonResponse(405, { error: "method_not_allowed" });
|
|
173
|
+
}
|
|
174
|
+
if (bearerOf(req) !== ingestSecret) {
|
|
175
|
+
return jsonResponse(401, { error: "unauthorized" });
|
|
176
|
+
}
|
|
177
|
+
const env = config.env ?? process.env;
|
|
178
|
+
const settled = await Promise.allSettled(
|
|
179
|
+
PROBE_SPECS.map((spec) => probeProvider(spec, env))
|
|
180
|
+
);
|
|
181
|
+
const keys = settled.map((s, i) => {
|
|
182
|
+
if (s.status === "fulfilled") return s.value;
|
|
183
|
+
const spec = PROBE_SPECS[i];
|
|
184
|
+
return {
|
|
185
|
+
provider: spec.provider,
|
|
186
|
+
env: spec.canonicalEnvName,
|
|
187
|
+
present: true,
|
|
188
|
+
valid: null,
|
|
189
|
+
detail: "probe_failed"
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
keys.sort((a, b) => a.provider.localeCompare(b.provider));
|
|
193
|
+
const body = {
|
|
194
|
+
app_id: appId,
|
|
195
|
+
checked_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
196
|
+
keys
|
|
197
|
+
};
|
|
198
|
+
return jsonResponse(200, body);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
void err;
|
|
201
|
+
return jsonResponse(500, { error: "key_health_internal_error" });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { handle };
|
|
205
|
+
}
|
|
206
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
207
|
+
0 && (module.exports = {
|
|
208
|
+
createKeyHealthRoute
|
|
209
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warmdrift/kgauto-compiler",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.59",
|
|
4
4
|
"description": "Prompt compiler + central learning brain for multi-model AI apps. Swap models without rewriting prompts.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
"import": "./dist/brain-proxy.mjs",
|
|
27
27
|
"require": "./dist/brain-proxy.js"
|
|
28
28
|
},
|
|
29
|
+
"./key-health": {
|
|
30
|
+
"types": "./dist/key-health.d.ts",
|
|
31
|
+
"import": "./dist/key-health.mjs",
|
|
32
|
+
"require": "./dist/key-health.js"
|
|
33
|
+
},
|
|
29
34
|
"./glassbox": {
|
|
30
35
|
"types": "./dist/glassbox/index.d.ts",
|
|
31
36
|
"import": "./dist/glassbox/index.mjs",
|
|
@@ -52,7 +57,7 @@
|
|
|
52
57
|
"README.md"
|
|
53
58
|
],
|
|
54
59
|
"scripts": {
|
|
55
|
-
"build": "tsup src/index.ts src/dialect.ts src/profiles.ts src/brain-proxy.ts src/glassbox/index.ts src/glassbox-routes/index.ts src/glassbox-routes/format.ts src/glassbox-routes/react/index.ts --format cjs,esm --dts --clean --external react --external react-dom",
|
|
60
|
+
"build": "tsup src/index.ts src/dialect.ts src/profiles.ts src/brain-proxy.ts src/key-health.ts src/glassbox/index.ts src/glassbox-routes/index.ts src/glassbox-routes/format.ts src/glassbox-routes/react/index.ts --format cjs,esm --dts --clean --external react --external react-dom",
|
|
56
61
|
"test": "vitest run",
|
|
57
62
|
"test:watch": "vitest",
|
|
58
63
|
"typecheck": "tsc --noEmit",
|