@warmdrift/kgauto-compiler 2.0.0-alpha.57 → 2.0.0-alpha.58

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.
@@ -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
 
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
 
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,
@@ -6935,6 +6936,188 @@ function createBrainForwardRoutes(config) {
6935
6936
  return { handle, segments: KNOWN_SEGMENTS };
6936
6937
  }
6937
6938
 
6939
+ // src/key-health.ts
6940
+ var JSON_HEADERS2 = { "Content-Type": "application/json" };
6941
+ function jsonResponse2(status, body) {
6942
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS2 });
6943
+ }
6944
+ function bearerOf2(req) {
6945
+ const header = req.headers.get("Authorization") ?? "";
6946
+ const match = /^Bearer\s+(.+)$/i.exec(header);
6947
+ return match?.[1]?.trim() ?? "";
6948
+ }
6949
+ function requireString2(name, value) {
6950
+ if (typeof value !== "string" || value.length === 0) {
6951
+ throw new Error(`createKeyHealthRoute: ${name} is required`);
6952
+ }
6953
+ return value;
6954
+ }
6955
+ function envKey(env, name) {
6956
+ const v = env[name];
6957
+ if (typeof v !== "string") return void 0;
6958
+ const trimmed = v.trim();
6959
+ return trimmed.length > 0 ? trimmed : void 0;
6960
+ }
6961
+ var PROBE_SPECS = [
6962
+ {
6963
+ provider: "anthropic",
6964
+ canonicalEnvName: "ANTHROPIC_API_KEY",
6965
+ buildRequest: (key) => ({
6966
+ url: "https://api.anthropic.com/v1/models?limit=1",
6967
+ headers: { "x-api-key": key, "anthropic-version": "2023-06-01" }
6968
+ })
6969
+ },
6970
+ {
6971
+ provider: "deepseek",
6972
+ canonicalEnvName: "DEEPSEEK_API_KEY",
6973
+ buildRequest: (key) => ({
6974
+ url: "https://api.deepseek.com/user/balance",
6975
+ headers: { Authorization: `Bearer ${key}` }
6976
+ }),
6977
+ parseBalanceUsd: (body) => {
6978
+ const infos = body?.balance_infos;
6979
+ if (!Array.isArray(infos) || infos.length === 0) return void 0;
6980
+ const first = infos[0];
6981
+ if (first?.currency !== "USD") return void 0;
6982
+ const n = Number(first.total_balance);
6983
+ return Number.isFinite(n) ? n : void 0;
6984
+ }
6985
+ },
6986
+ {
6987
+ provider: "google",
6988
+ canonicalEnvName: "GEMINI_API_KEY",
6989
+ buildRequest: (key) => ({
6990
+ url: `https://generativelanguage.googleapis.com/v1beta/models?pageSize=1&key=${encodeURIComponent(key)}`,
6991
+ headers: {}
6992
+ })
6993
+ },
6994
+ {
6995
+ provider: "openai",
6996
+ canonicalEnvName: "OPENAI_API_KEY",
6997
+ buildRequest: (key) => ({
6998
+ url: "https://api.openai.com/v1/models",
6999
+ headers: { Authorization: `Bearer ${key}` }
7000
+ })
7001
+ }
7002
+ ];
7003
+ function createKeyHealthRoute(config) {
7004
+ const appId = requireString2("appId", config.appId);
7005
+ const ingestSecret = requireString2("ingestSecret", config.ingestSecret);
7006
+ const fetchFn = config.fetchImpl ?? fetch;
7007
+ const timeoutMs = config.timeoutMs ?? 3e3;
7008
+ async function probeProvider(spec, env) {
7009
+ let key;
7010
+ let envName = spec.canonicalEnvName;
7011
+ if (spec.provider === "google") {
7012
+ const gemini = envKey(env, "GEMINI_API_KEY");
7013
+ const google = envKey(env, "GOOGLE_API_KEY");
7014
+ const aiSdk = envKey(env, "GOOGLE_GENERATIVE_AI_API_KEY");
7015
+ key = gemini ?? google ?? aiSdk;
7016
+ envName = gemini ? "GEMINI_API_KEY" : google ? "GOOGLE_API_KEY" : aiSdk ? "GOOGLE_GENERATIVE_AI_API_KEY" : "GEMINI_API_KEY";
7017
+ } else {
7018
+ key = envKey(env, spec.canonicalEnvName);
7019
+ }
7020
+ if (!key) {
7021
+ return {
7022
+ provider: spec.provider,
7023
+ env: envName,
7024
+ present: false,
7025
+ valid: null,
7026
+ detail: "key_absent"
7027
+ };
7028
+ }
7029
+ const base = {
7030
+ provider: spec.provider,
7031
+ env: envName,
7032
+ present: true,
7033
+ valid: null
7034
+ };
7035
+ const { url, headers } = spec.buildRequest(key);
7036
+ const started = Date.now();
7037
+ let res;
7038
+ try {
7039
+ res = await fetchFn(url, {
7040
+ method: "GET",
7041
+ headers,
7042
+ signal: AbortSignal.timeout(timeoutMs)
7043
+ });
7044
+ } catch (err) {
7045
+ const isTimeout = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
7046
+ return {
7047
+ ...base,
7048
+ latency_ms: Date.now() - started,
7049
+ // NEVER echo err.message — provider errors could theoretically carry
7050
+ // request context; a fixed vocabulary keeps key material impossible.
7051
+ detail: isTimeout ? "timeout" : "network_error"
7052
+ };
7053
+ }
7054
+ const latencyMs = Date.now() - started;
7055
+ if (res.ok) {
7056
+ const result = {
7057
+ ...base,
7058
+ valid: true,
7059
+ status: res.status,
7060
+ latency_ms: latencyMs
7061
+ };
7062
+ if (spec.parseBalanceUsd) {
7063
+ try {
7064
+ const body = await res.json();
7065
+ const balance = spec.parseBalanceUsd(body);
7066
+ if (balance !== void 0) result.balance_usd = balance;
7067
+ } catch {
7068
+ }
7069
+ }
7070
+ return result;
7071
+ }
7072
+ if (res.status === 401 || res.status === 403) {
7073
+ return { ...base, valid: false, status: res.status, latency_ms: latencyMs };
7074
+ }
7075
+ return {
7076
+ ...base,
7077
+ valid: null,
7078
+ status: res.status,
7079
+ latency_ms: latencyMs,
7080
+ detail: `http_${res.status}`
7081
+ };
7082
+ }
7083
+ async function handle(req) {
7084
+ try {
7085
+ if (req.method !== "GET") {
7086
+ return jsonResponse2(405, { error: "method_not_allowed" });
7087
+ }
7088
+ if (bearerOf2(req) !== ingestSecret) {
7089
+ return jsonResponse2(401, { error: "unauthorized" });
7090
+ }
7091
+ const env = config.env ?? process.env;
7092
+ const settled = await Promise.allSettled(
7093
+ PROBE_SPECS.map((spec) => probeProvider(spec, env))
7094
+ );
7095
+ const keys = settled.map((s, i) => {
7096
+ if (s.status === "fulfilled") return s.value;
7097
+ const spec = PROBE_SPECS[i];
7098
+ return {
7099
+ provider: spec.provider,
7100
+ env: spec.canonicalEnvName,
7101
+ present: true,
7102
+ valid: null,
7103
+ detail: "probe_failed"
7104
+ };
7105
+ });
7106
+ keys.sort((a, b) => a.provider.localeCompare(b.provider));
7107
+ const body = {
7108
+ app_id: appId,
7109
+ checked_at: (/* @__PURE__ */ new Date()).toISOString(),
7110
+ keys
7111
+ };
7112
+ return jsonResponse2(200, body);
7113
+ } catch (err) {
7114
+ void err;
7115
+ return jsonResponse2(500, { error: "key_health_internal_error" });
7116
+ }
7117
+ }
7118
+ return { handle };
7119
+ }
7120
+
6938
7121
  // src/oracle.ts
6939
7122
  var DEFAULT_DIMENSIONS = ["correctness", "completeness", "conciseness", "format"];
6940
7123
  var judgeCallTimes = [];
@@ -7395,6 +7578,7 @@ function compile2(ir, opts) {
7395
7578
  configureBrain,
7396
7579
  countTokens,
7397
7580
  createBrainForwardRoutes,
7581
+ createKeyHealthRoute,
7398
7582
  deriveFamilyFromModelId,
7399
7583
  deriveOwnership,
7400
7584
  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,
@@ -4673,6 +4676,7 @@ export {
4673
4676
  configureBrain,
4674
4677
  countTokens,
4675
4678
  createBrainForwardRoutes,
4679
+ createKeyHealthRoute,
4676
4680
  deriveFamilyFromModelId,
4677
4681
  deriveOwnership,
4678
4682
  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
+ });
@@ -0,0 +1,6 @@
1
+ import {
2
+ createKeyHealthRoute
3
+ } from "./chunk-B4INZ32V.mjs";
4
+ export {
5
+ createKeyHealthRoute
6
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warmdrift/kgauto-compiler",
3
- "version": "2.0.0-alpha.57",
3
+ "version": "2.0.0-alpha.58",
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",