@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.
- package/README.md +43 -19
- package/dist/cli/lifecycle.js +1 -1
- package/dist/gateway/config.d.ts +6 -0
- package/dist/gateway/config.js +67 -5
- package/dist/gateway/http.d.ts +2 -0
- package/dist/gateway/http.js +31 -0
- package/dist/gateway/index.d.ts +1 -1
- package/dist/gateway/index.js +1 -1
- package/dist/gateway/openai-adapter.js +9 -4
- package/dist/gateway/types.d.ts +1 -0
- package/dist/harness/session.d.ts +1 -1
- package/dist/harness/session.js +1 -1
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.js +1 -1
- package/dist/tools/types.js +15 -0
- package/dist/ui/csp-hashes.json +5 -5
- package/dist/ui/deps.d.ts +2 -2
- package/dist/ui/gateway-setup.js +132 -22
- package/dist/ui/static/404.html +1 -1
- package/dist/ui/static/_next/static/chunks/44-5a78c4df1a436207.js +1 -0
- package/dist/ui/static/index.html +1 -1
- package/dist/ui/static/index.txt +2 -2
- package/dist/ui/static/launch.html +1 -1
- package/dist/ui/static/launch.txt +2 -2
- package/package.json +1 -1
- package/dist/ui/static/_next/static/chunks/44-6f8fe2615a484a49.js +0 -1
- /package/dist/ui/static/_next/static/{krMnUnw0yLSRAH4ny0AJo → _up3xgs7rHI2vcDDL0kQ3}/_buildManifest.js +0 -0
- /package/dist/ui/static/_next/static/{krMnUnw0yLSRAH4ny0AJo → _up3xgs7rHI2vcDDL0kQ3}/_ssgManifest.js +0 -0
package/dist/ui/gateway-setup.js
CHANGED
|
@@ -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
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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
|
|
143
|
-
const response = await gatewayFetch(
|
|
192
|
+
async function fetchDiscoveryJson(url, apiKey, apiKeyHeaderName) {
|
|
193
|
+
const response = await gatewayFetch(url, {
|
|
144
194
|
method: "GET",
|
|
145
|
-
headers:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/ui/static/404.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE 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,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";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>
|