@oscharko-dev/keiko 0.1.3 → 0.1.4

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.
@@ -4,15 +4,18 @@
4
4
  // runtime config without exposing credentials back to the browser.
5
5
  import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
6
6
  import { dirname } from "node:path";
7
- import { Gateway, createDefaultChatCapability, listConfiguredCapabilities, parseGatewayConfig, toSafeObject, } from "../gateway/index.js";
8
- import { gatewayFetch } from "../gateway/http.js";
7
+ import { apiKeyHeaderValue, ConfigInvalidError, DEFAULT_API_KEY_HEADER_NAME, Gateway, createDefaultChatCapability, listConfiguredCapabilities, normalizeApiKeyHeaderName, parseGatewayConfig, toSafeObject, validateBaseUrl, } from "../gateway/index.js";
8
+ import { gatewayFetch, readJsonCapped } from "../gateway/http.js";
9
9
  import { redact } from "../gateway/redaction.js";
10
10
  import { errorBody } from "./routes.js";
11
11
  const MAX_BODY_BYTES = 64_000;
12
12
  const MAX_DISCOVERED_MODELS = 100;
13
+ const MAX_DEPLOYMENT_NAMES = 100;
14
+ const MAX_MODEL_ID_LENGTH = 160;
13
15
  const DISCOVERED_MODEL_SMOKE_TIMEOUT_MS = 15_000;
14
16
  const DEPLOYMENT_SMOKE_TIMEOUT_MS = 30_000;
15
17
  const SETUP_SMOKE_CONCURRENCY = 4;
18
+ const CHAT_COMPATIBLE_MODES = new Set(["chat", "completion", "responses"]);
16
19
  class BodyTooLargeError extends Error {
17
20
  constructor() {
18
21
  super("request body too large");
@@ -93,6 +96,7 @@ function providerRaw(modelId, baseUrl, apiKey, options = {}) {
93
96
  modelId,
94
97
  baseUrl,
95
98
  apiKey,
99
+ apiKeyHeaderName: options.apiKeyHeaderName ?? DEFAULT_API_KEY_HEADER_NAME,
96
100
  capability: createDefaultChatCapability(modelId),
97
101
  timeoutMs: options.timeoutMs ?? 30_000,
98
102
  maxRetries: options.maxRetries ?? 2,
@@ -108,19 +112,65 @@ function buildRawConfig(baseUrl, apiKey, modelIds, options = {}) {
108
112
  function modelsEndpoint(baseUrl) {
109
113
  return `${baseUrl}/models`;
110
114
  }
111
- function modelIdFromDiscoveryItem(item) {
112
- if (!isRecord(item) || typeof item.id !== "string") {
113
- return undefined;
115
+ function modelInfoEndpointCandidates(baseUrl) {
116
+ const normalized = normalizeBaseUrl(baseUrl);
117
+ return [`${normalized}/model/info`];
118
+ }
119
+ function apiKeyHeaders(apiKey, apiKeyHeaderName) {
120
+ return { [apiKeyHeaderName]: apiKeyHeaderValue(apiKeyHeaderName, apiKey) };
121
+ }
122
+ function hasDisallowedModelIdCharacter(id) {
123
+ for (let index = 0; index < id.length; index += 1) {
124
+ const code = id.charCodeAt(index);
125
+ if (code <= 31 || code === 127) {
126
+ return true;
127
+ }
114
128
  }
115
- const id = item.id.trim();
116
- if (id.length === 0) {
117
- return undefined;
129
+ return false;
130
+ }
131
+ function isUsableModelId(id) {
132
+ return id.length > 0 && id.length <= MAX_MODEL_ID_LENGTH && !hasDisallowedModelIdCharacter(id);
133
+ }
134
+ function modelIdFromKnownFields(item) {
135
+ for (const field of ["id", "model_name", "model", "deployment_name", "deploymentName"]) {
136
+ const value = item[field];
137
+ if (typeof value === "string") {
138
+ const id = value.trim();
139
+ if (isUsableModelId(id)) {
140
+ return id;
141
+ }
142
+ }
143
+ }
144
+ return undefined;
145
+ }
146
+ function nestedRecord(value, key) {
147
+ const nested = value[key];
148
+ return isRecord(nested) ? nested : undefined;
149
+ }
150
+ function modelModeFromDiscoveryItem(item) {
151
+ const modelInfo = nestedRecord(item, "model_info");
152
+ const litellmParams = nestedRecord(item, "litellm_params");
153
+ const candidates = [item.mode, modelInfo?.mode, litellmParams?.mode];
154
+ for (const candidate of candidates) {
155
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
156
+ return candidate.trim().toLowerCase();
157
+ }
118
158
  }
159
+ return undefined;
160
+ }
161
+ function isExplicitlyNonChatModel(item) {
119
162
  const capabilities = isRecord(item.capabilities) ? item.capabilities : undefined;
120
163
  if (capabilities?.chat_completion === false) {
164
+ return true;
165
+ }
166
+ const mode = modelModeFromDiscoveryItem(item);
167
+ return mode !== undefined && !CHAT_COMPATIBLE_MODES.has(mode);
168
+ }
169
+ function modelIdFromDiscoveryItem(item) {
170
+ if (!isRecord(item) || isExplicitlyNonChatModel(item)) {
121
171
  return undefined;
122
172
  }
123
- return id;
173
+ return modelIdFromKnownFields(item);
124
174
  }
125
175
  function parseModelList(payload) {
126
176
  if (!isRecord(payload) || !Array.isArray(payload.data)) {
@@ -139,23 +189,40 @@ function parseModelList(payload) {
139
189
  }
140
190
  return unique.slice(0, MAX_DISCOVERED_MODELS);
141
191
  }
142
- async function defaultGatewayModelDiscovery(baseUrl, apiKey) {
143
- const response = await gatewayFetch(modelsEndpoint(baseUrl), {
192
+ async function fetchDiscoveryJson(url, apiKey, apiKeyHeaderName) {
193
+ const response = await gatewayFetch(url, {
144
194
  method: "GET",
145
- headers: { authorization: `Bearer ${apiKey}` },
195
+ headers: apiKeyHeaders(apiKey, apiKeyHeaderName),
146
196
  signal: AbortSignal.timeout(30_000),
147
197
  });
148
198
  if (!response.ok) {
149
199
  throw new Error(`model discovery returned HTTP ${String(response.status)}`);
150
200
  }
151
- let payload;
152
201
  try {
153
- payload = await response.json();
202
+ return await readJsonCapped(response);
154
203
  }
155
204
  catch {
156
205
  throw new Error("model discovery response was not readable JSON");
157
206
  }
158
- return parseModelList(payload);
207
+ }
208
+ async function discoverLiteLlmModelInfo(baseUrl, apiKey, apiKeyHeaderName) {
209
+ for (const endpoint of modelInfoEndpointCandidates(baseUrl)) {
210
+ try {
211
+ return parseModelList(await fetchDiscoveryJson(endpoint, apiKey, apiKeyHeaderName));
212
+ }
213
+ catch {
214
+ // /model/info is a LiteLLM-specific enrichment endpoint. If it is absent or blocked,
215
+ // continue with OpenAI-compatible /models discovery so customer gateways are not broken.
216
+ }
217
+ }
218
+ return undefined;
219
+ }
220
+ async function defaultGatewayModelDiscovery(baseUrl, apiKey, apiKeyHeaderName = DEFAULT_API_KEY_HEADER_NAME) {
221
+ const litellmModels = await discoverLiteLlmModelInfo(baseUrl, apiKey, apiKeyHeaderName);
222
+ if (litellmModels !== undefined) {
223
+ return litellmModels;
224
+ }
225
+ return parseModelList(await fetchDiscoveryJson(modelsEndpoint(baseUrl), apiKey, apiKeyHeaderName));
159
226
  }
160
227
  function deploymentNameValues(value) {
161
228
  if (typeof value === "string") {
@@ -180,7 +247,32 @@ function parseDeploymentNames(value) {
180
247
  body: errorBody("BAD_REQUEST", "deploymentNames must be a string or an array of strings."),
181
248
  };
182
249
  }
183
- return normalizeDeploymentNames(values);
250
+ const names = normalizeDeploymentNames(values);
251
+ if (names.length > MAX_DEPLOYMENT_NAMES) {
252
+ return {
253
+ status: 400,
254
+ body: errorBody("BAD_REQUEST", "deploymentNames exceeds the model setup limit."),
255
+ };
256
+ }
257
+ if (names.some((name) => !isUsableModelId(name))) {
258
+ return {
259
+ status: 400,
260
+ body: errorBody("BAD_REQUEST", "deploymentNames contains an invalid model id."),
261
+ };
262
+ }
263
+ return names;
264
+ }
265
+ function validateSetupConnection(baseUrl, apiKey, apiKeyHeaderName) {
266
+ try {
267
+ parseGatewayConfig(buildRawConfig(baseUrl, apiKey, ["setup-validation"], { apiKeyHeaderName }));
268
+ return undefined;
269
+ }
270
+ catch (error) {
271
+ if (error instanceof ConfigInvalidError) {
272
+ return { status: 400, body: errorBody("BAD_REQUEST", error.message) };
273
+ }
274
+ throw error;
275
+ }
184
276
  }
185
277
  async function defaultGatewaySetupTester(config, candidateModelIds) {
186
278
  const gateway = new Gateway(config);
@@ -234,11 +326,25 @@ function readSetupRequest(raw) {
234
326
  if (baseUrl.length === 0 || apiKey.length === 0) {
235
327
  return { status: 400, body: errorBody("BAD_REQUEST", "baseUrl and apiKey are required.") };
236
328
  }
329
+ let apiKeyHeaderName;
330
+ try {
331
+ apiKeyHeaderName = normalizeApiKeyHeaderName(raw.apiKeyHeaderName, "apiKeyHeaderName", DEFAULT_API_KEY_HEADER_NAME);
332
+ }
333
+ catch (error) {
334
+ if (error instanceof ConfigInvalidError) {
335
+ return { status: 400, body: errorBody("BAD_REQUEST", error.message) };
336
+ }
337
+ throw error;
338
+ }
237
339
  const deploymentNames = parseDeploymentNames(raw.deploymentNames);
238
340
  if ("status" in deploymentNames) {
239
341
  return deploymentNames;
240
342
  }
241
- return { baseUrl, apiKey, deploymentNames };
343
+ const invalidConnection = validateSetupConnection(baseUrl, apiKey, apiKeyHeaderName);
344
+ if (invalidConnection !== undefined) {
345
+ return invalidConnection;
346
+ }
347
+ return { baseUrl, apiKey, apiKeyHeaderName, deploymentNames };
242
348
  }
243
349
  function safeError(error, secrets) {
244
350
  if (error instanceof Error) {
@@ -246,16 +352,20 @@ function safeError(error, secrets) {
246
352
  }
247
353
  return "Gateway setup failed.";
248
354
  }
249
- async function verifySetupCandidate(baseUrl, apiKey, deploymentNames, tester, discovery) {
250
- const candidateModelIds = deploymentNames.length > 0 ? deploymentNames : await discovery(baseUrl, apiKey);
355
+ async function verifySetupCandidate(baseUrl, apiKey, apiKeyHeaderName, deploymentNames, tester, discovery) {
356
+ // Defence-in-depth: never send the credential to a candidate URL that has not passed the same
357
+ // scheme/credential/loopback validation as the originally submitted base URL.
358
+ validateBaseUrl(baseUrl, "candidate");
359
+ const candidateModelIds = deploymentNames.length > 0 ? deploymentNames : await discovery(baseUrl, apiKey, apiKeyHeaderName);
251
360
  const smokeTimeoutMs = deploymentNames.length > 0 ? DEPLOYMENT_SMOKE_TIMEOUT_MS : DISCOVERED_MODEL_SMOKE_TIMEOUT_MS;
252
361
  const candidateRawConfig = buildRawConfig(baseUrl, apiKey, candidateModelIds, {
362
+ apiKeyHeaderName,
253
363
  timeoutMs: smokeTimeoutMs,
254
364
  maxRetries: 0,
255
365
  });
256
366
  const candidateConfig = parseGatewayConfig(candidateRawConfig);
257
367
  const testedModelIds = await tester(candidateConfig, candidateModelIds);
258
- const rawConfig = buildRawConfig(baseUrl, apiKey, testedModelIds);
368
+ const rawConfig = buildRawConfig(baseUrl, apiKey, testedModelIds, { apiKeyHeaderName });
259
369
  const config = parseGatewayConfig(rawConfig);
260
370
  return { rawConfig, config, testedModelIds };
261
371
  }
@@ -331,13 +441,13 @@ export async function handleGatewaySetup(ctx, deps) {
331
441
  const errors = [];
332
442
  for (const baseUrl of baseUrlCandidates) {
333
443
  try {
334
- const verified = await verifySetupCandidate(baseUrl, request.apiKey, request.deploymentNames, tester, discovery);
444
+ const verified = await verifySetupCandidate(baseUrl, request.apiKey, request.apiKeyHeaderName, request.deploymentNames, tester, discovery);
335
445
  savePrivateJson(deps.gatewayConfig.storagePath, verified.rawConfig);
336
446
  deps.gatewayConfig.set(verified.config, true);
337
447
  return setupSuccessResult(verified.config, verified.testedModelIds);
338
448
  }
339
449
  catch (error) {
340
- errors.push(`candidate ${String(errors.length + 1)}: ${safeError(error, [request.apiKey, baseUrl])}`);
450
+ errors.push(`candidate ${String(errors.length + 1)}: ${safeError(error, [request.apiKey, request.baseUrl, baseUrl])}`);
341
451
  }
342
452
  }
343
453
  return setupFailureResult(errors);
@@ -1 +1 @@
1
- <!DOCTYPE html><!--krMnUnw0yLSRAH4ny0AJo--><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/3f36649d8ca8f3e7.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-4a462cecab786e93.js"/><script src="/_next/static/chunks/4bd1b696-c023c6e3521b1417.js" async=""></script><script src="/_next/static/chunks/255-d47fd57964443afe.js" async=""></script><script src="/_next/static/chunks/main-app-e8144a306630b76d.js" async=""></script><meta name="robots" content="noindex"/><title>404: This page could not be found.</title><title>Keiko</title><meta name="description" content="Keiko local developer-assist workspace."/><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body><div hidden=""><!--$--><!--/$--></div><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><!--$--><!--/$--><script src="/_next/static/chunks/webpack-4a462cecab786e93.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[9766,[],\"\"]\n3:I[8924,[],\"\"]\n4:I[4431,[],\"OutletBoundary\"]\n6:I[5278,[],\"AsyncMetadataOutlet\"]\n8:I[4431,[],\"ViewportBoundary\"]\na:I[4431,[],\"MetadataBoundary\"]\nb:\"$Sreact.suspense\"\nd:I[7150,[],\"\"]\n:HL[\"/_next/static/css/3f36649d8ca8f3e7.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"krMnUnw0yLSRAH4ny0AJo\",\"p\":\"\",\"c\":[\"\",\"_not-found\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/3f36649d8ca8f3e7.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"children\":[\"$\",\"$L2\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L3\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]]}],{\"children\":[\"/_not-found\",[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L2\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L3\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],null,[\"$\",\"$L4\",null,{\"children\":[\"$L5\",[\"$\",\"$L6\",null,{\"promise\":\"$@7\"}]]}]]}],{},null,false]},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],[[\"$\",\"$L8\",null,{\"children\":\"$L9\"}],null],[\"$\",\"$La\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$b\",null,{\"fallback\":null,\"children\":\"$Lc\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$d\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"9:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n5:null\n"])</script><script>self.__next_f.push([1,"7:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Keiko\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Keiko local developer-assist workspace.\"}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"c:\"$7:metadata\"\n"])</script></body></html>
1
+ <!DOCTYPE html><!--_up3xgs7rHI2vcDDL0kQ3--><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/3f36649d8ca8f3e7.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-4a462cecab786e93.js"/><script src="/_next/static/chunks/4bd1b696-c023c6e3521b1417.js" async=""></script><script src="/_next/static/chunks/255-d47fd57964443afe.js" async=""></script><script src="/_next/static/chunks/main-app-e8144a306630b76d.js" async=""></script><meta name="robots" content="noindex"/><title>404: This page could not be found.</title><title>Keiko</title><meta name="description" content="Keiko local developer-assist workspace."/><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body><div hidden=""><!--$--><!--/$--></div><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><!--$--><!--/$--><script src="/_next/static/chunks/webpack-4a462cecab786e93.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[9766,[],\"\"]\n3:I[8924,[],\"\"]\n4:I[4431,[],\"OutletBoundary\"]\n6:I[5278,[],\"AsyncMetadataOutlet\"]\n8:I[4431,[],\"ViewportBoundary\"]\na:I[4431,[],\"MetadataBoundary\"]\nb:\"$Sreact.suspense\"\nd:I[7150,[],\"\"]\n:HL[\"/_next/static/css/3f36649d8ca8f3e7.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"_up3xgs7rHI2vcDDL0kQ3\",\"p\":\"\",\"c\":[\"\",\"_not-found\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/3f36649d8ca8f3e7.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"children\":[\"$\",\"$L2\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L3\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]]}],{\"children\":[\"/_not-found\",[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L2\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L3\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],null,[\"$\",\"$L4\",null,{\"children\":[\"$L5\",[\"$\",\"$L6\",null,{\"promise\":\"$@7\"}]]}]]}],{},null,false]},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],[[\"$\",\"$L8\",null,{\"children\":\"$L9\"}],null],[\"$\",\"$La\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$b\",null,{\"fallback\":null,\"children\":\"$Lc\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$d\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"9:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n5:null\n"])</script><script>self.__next_f.push([1,"7:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Keiko\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Keiko local developer-assist workspace.\"}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"c:\"$7:metadata\"\n"])</script></body></html>