@oscharko-dev/keiko 0.1.0-beta.3 → 0.1.2
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/gateway/http.d.ts +6 -0
- package/dist/gateway/http.js +111 -0
- package/dist/gateway/openai-adapter.d.ts +0 -3
- package/dist/gateway/openai-adapter.js +3 -71
- 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/ui/csp-hashes.json +7 -7
- package/dist/ui/gateway-setup.js +148 -39
- package/dist/ui/static/404.html +1 -1
- package/dist/ui/static/_next/static/chunks/44-534236109b98f09a.js +1 -0
- package/dist/ui/static/_next/static/css/{3d68155c8db012f4.css → 3f36649d8ca8f3e7.css} +1 -1
- package/dist/ui/static/index.html +1 -1
- package/dist/ui/static/index.txt +3 -3
- package/dist/ui/static/launch.html +1 -1
- package/dist/ui/static/launch.txt +3 -3
- package/package.json +1 -1
- package/dist/ui/static/_next/static/chunks/44-17c259c8e72fb82f.js +0 -1
- /package/dist/ui/static/_next/static/{f456ZUOjzfLnTnTyaLylj → pBbsB1CYvKAPHJwR_PXZP}/_buildManifest.js +0 -0
- /package/dist/ui/static/_next/static/{f456ZUOjzfLnTnTyaLylj → pBbsB1CYvKAPHJwR_PXZP}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface GatewayFetchOptions extends RequestInit {
|
|
2
|
+
readonly fetchImpl?: typeof fetch | undefined;
|
|
3
|
+
readonly useCaFallback?: boolean | undefined;
|
|
4
|
+
}
|
|
5
|
+
export declare function isMissingIssuerError(error: unknown): boolean;
|
|
6
|
+
export declare function gatewayFetch(url: string, options?: GatewayFetchOptions): Promise<Response>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { request as httpsRequest } from "node:https";
|
|
3
|
+
import { rootCertificates } from "node:tls";
|
|
4
|
+
function headersFromNode(headers) {
|
|
5
|
+
const out = new Headers();
|
|
6
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
7
|
+
if (Array.isArray(value)) {
|
|
8
|
+
for (const item of value)
|
|
9
|
+
out.append(name, item);
|
|
10
|
+
}
|
|
11
|
+
else if (value !== undefined) {
|
|
12
|
+
out.set(name, value);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
function headersToRecord(headers) {
|
|
18
|
+
const out = {};
|
|
19
|
+
const normalized = new Headers(headers);
|
|
20
|
+
normalized.forEach((value, key) => {
|
|
21
|
+
out[key] = value;
|
|
22
|
+
});
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
function isRecord(value) {
|
|
26
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
export function isMissingIssuerError(error) {
|
|
29
|
+
const cause = isRecord(error) ? error.cause : undefined;
|
|
30
|
+
const candidates = [error, cause];
|
|
31
|
+
return candidates.some((item) => {
|
|
32
|
+
if (!isRecord(item))
|
|
33
|
+
return false;
|
|
34
|
+
return item.code === "UNABLE_TO_GET_ISSUER_CERT_LOCALLY";
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function usesHttps(url) {
|
|
38
|
+
try {
|
|
39
|
+
return new URL(url).protocol === "https:";
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function extraCaCertificates() {
|
|
46
|
+
const path = process.env.NODE_EXTRA_CA_CERTS;
|
|
47
|
+
if (path === undefined || path.trim().length === 0) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return [readFileSync(path, "utf8")];
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function caBundle() {
|
|
58
|
+
return [...rootCertificates, ...extraCaCertificates()];
|
|
59
|
+
}
|
|
60
|
+
function bodyToString(body) {
|
|
61
|
+
if (body === undefined || body === null) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
if (typeof body === "string") {
|
|
65
|
+
return body;
|
|
66
|
+
}
|
|
67
|
+
if (body instanceof URLSearchParams) {
|
|
68
|
+
return body.toString();
|
|
69
|
+
}
|
|
70
|
+
throw new TypeError("gateway HTTP fallback supports string request bodies only");
|
|
71
|
+
}
|
|
72
|
+
function fetchWithCaBundle(url, init) {
|
|
73
|
+
const body = bodyToString(init.body);
|
|
74
|
+
const headers = headersToRecord(init.headers);
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const req = httpsRequest(url, {
|
|
77
|
+
method: init.method ?? "GET",
|
|
78
|
+
headers,
|
|
79
|
+
ca: [...caBundle()],
|
|
80
|
+
signal: init.signal ?? undefined,
|
|
81
|
+
}, (res) => {
|
|
82
|
+
const chunks = [];
|
|
83
|
+
res.on("data", (chunk) => {
|
|
84
|
+
chunks.push(chunk);
|
|
85
|
+
});
|
|
86
|
+
res.on("end", () => {
|
|
87
|
+
resolve(new Response(Buffer.concat(chunks), {
|
|
88
|
+
status: res.statusCode ?? 500,
|
|
89
|
+
statusText: res.statusMessage ?? "",
|
|
90
|
+
headers: headersFromNode(res.headers),
|
|
91
|
+
}));
|
|
92
|
+
});
|
|
93
|
+
res.on("error", reject);
|
|
94
|
+
});
|
|
95
|
+
req.on("error", reject);
|
|
96
|
+
req.end(body);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
export async function gatewayFetch(url, options = {}) {
|
|
100
|
+
const { fetchImpl, useCaFallback = fetchImpl === undefined, ...init } = options;
|
|
101
|
+
const doFetch = fetchImpl ?? globalThis.fetch;
|
|
102
|
+
try {
|
|
103
|
+
return await doFetch(url, init);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (useCaFallback && usesHttps(url) && isMissingIssuerError(error)) {
|
|
107
|
+
return fetchWithCaBundle(url, init);
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -7,13 +7,10 @@ export interface AdapterDeps {
|
|
|
7
7
|
}
|
|
8
8
|
export declare class OpenAiAdapter implements ProviderAdapter {
|
|
9
9
|
private readonly deps;
|
|
10
|
-
private readonly fetchImpl;
|
|
11
10
|
private readonly now;
|
|
12
|
-
private readonly useBundledCaFallback;
|
|
13
11
|
constructor(deps: AdapterDeps);
|
|
14
12
|
call: (request: GatewayRequest, config: ModelProviderConfig) => Promise<NormalizedResponse>;
|
|
15
13
|
private dispatch;
|
|
16
|
-
private dispatchWithBundledCa;
|
|
17
14
|
private mapDispatchError;
|
|
18
15
|
private readBody;
|
|
19
16
|
private readErrorBody;
|
|
@@ -2,41 +2,10 @@
|
|
|
2
2
|
// AbortSignal. fetch, clock, request-id, and cost class are injected so tests run
|
|
3
3
|
// with no network I/O and no real time. The raw provider body is never echoed into
|
|
4
4
|
// an error; only a redacted, status-level summary is surfaced.
|
|
5
|
-
import { request as httpsRequest } from "node:https";
|
|
6
|
-
import { rootCertificates } from "node:tls";
|
|
7
5
|
import { AuthenticationError, CancelledError, ContextOverflowError, ModelRefusalError, ProviderError, RateLimitError, TimeoutError, TransportError, } from "./errors.js";
|
|
6
|
+
import { gatewayFetch } from "./http.js";
|
|
8
7
|
import { normalizeChatResponse } from "./normalize.js";
|
|
9
8
|
import { redact } from "./redaction.js";
|
|
10
|
-
function headersFromNode(headers) {
|
|
11
|
-
const out = new Headers();
|
|
12
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
13
|
-
if (Array.isArray(value)) {
|
|
14
|
-
for (const item of value)
|
|
15
|
-
out.append(name, item);
|
|
16
|
-
}
|
|
17
|
-
else if (value !== undefined) {
|
|
18
|
-
out.set(name, value);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return out;
|
|
22
|
-
}
|
|
23
|
-
function isMissingIssuerError(error) {
|
|
24
|
-
const cause = isRecord(error) ? error.cause : undefined;
|
|
25
|
-
const candidates = [error, cause];
|
|
26
|
-
return candidates.some((item) => {
|
|
27
|
-
if (!isRecord(item))
|
|
28
|
-
return false;
|
|
29
|
-
return item.code === "UNABLE_TO_GET_ISSUER_CERT_LOCALLY";
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
function usesHttps(url) {
|
|
33
|
-
try {
|
|
34
|
-
return new URL(url).protocol === "https:";
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
9
|
function buildMessage(message) {
|
|
41
10
|
const toolCalls = message.toolCalls?.map((call) => ({
|
|
42
11
|
id: call.id,
|
|
@@ -149,14 +118,10 @@ function mapHttpError(response, modelId, secrets, payload) {
|
|
|
149
118
|
}
|
|
150
119
|
export class OpenAiAdapter {
|
|
151
120
|
deps;
|
|
152
|
-
fetchImpl;
|
|
153
121
|
now;
|
|
154
|
-
useBundledCaFallback;
|
|
155
122
|
constructor(deps) {
|
|
156
123
|
this.deps = deps;
|
|
157
|
-
this.fetchImpl = deps.fetchImpl ?? globalThis.fetch;
|
|
158
124
|
this.now = deps.now ?? Date.now;
|
|
159
|
-
this.useBundledCaFallback = deps.fetchImpl === undefined;
|
|
160
125
|
}
|
|
161
126
|
call = async (request, config) => {
|
|
162
127
|
const secrets = [config.apiKey, config.baseUrl];
|
|
@@ -187,51 +152,18 @@ export class OpenAiAdapter {
|
|
|
187
152
|
authorization: `Bearer ${config.apiKey}`,
|
|
188
153
|
};
|
|
189
154
|
try {
|
|
190
|
-
return await
|
|
155
|
+
return await gatewayFetch(url, {
|
|
191
156
|
method: "POST",
|
|
192
157
|
headers,
|
|
193
158
|
body,
|
|
194
159
|
signal,
|
|
160
|
+
fetchImpl: this.deps.fetchImpl,
|
|
195
161
|
});
|
|
196
162
|
}
|
|
197
163
|
catch (error) {
|
|
198
|
-
if (this.useBundledCaFallback && usesHttps(url) && isMissingIssuerError(error)) {
|
|
199
|
-
try {
|
|
200
|
-
// Some shared-OpenSSL Node builds do not use Node's bundled public CA set by default.
|
|
201
|
-
return await this.dispatchWithBundledCa(url, headers, body, signal);
|
|
202
|
-
}
|
|
203
|
-
catch (fallbackError) {
|
|
204
|
-
throw this.mapDispatchError(fallbackError, config, cancel, timeoutSignal, secrets);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
164
|
throw this.mapDispatchError(error, config, cancel, timeoutSignal, secrets);
|
|
208
165
|
}
|
|
209
166
|
}
|
|
210
|
-
dispatchWithBundledCa(url, headers, body, signal) {
|
|
211
|
-
return new Promise((resolve, reject) => {
|
|
212
|
-
const req = httpsRequest(url, {
|
|
213
|
-
method: "POST",
|
|
214
|
-
headers,
|
|
215
|
-
ca: Array.from(rootCertificates),
|
|
216
|
-
signal,
|
|
217
|
-
}, (res) => {
|
|
218
|
-
const chunks = [];
|
|
219
|
-
res.on("data", (chunk) => {
|
|
220
|
-
chunks.push(chunk);
|
|
221
|
-
});
|
|
222
|
-
res.on("end", () => {
|
|
223
|
-
resolve(new Response(Buffer.concat(chunks), {
|
|
224
|
-
status: res.statusCode ?? 500,
|
|
225
|
-
statusText: res.statusMessage ?? "",
|
|
226
|
-
headers: headersFromNode(res.headers),
|
|
227
|
-
}));
|
|
228
|
-
});
|
|
229
|
-
res.on("error", reject);
|
|
230
|
-
});
|
|
231
|
-
req.on("error", reject);
|
|
232
|
-
req.end(body);
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
167
|
mapDispatchError(error, config, cancel, timeout, secrets) {
|
|
236
168
|
if (cancel?.aborted === true) {
|
|
237
169
|
return new CancelledError(`request for '${config.modelId}' cancelled`, secrets);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Clock } from "../gateway/types.js";
|
|
2
2
|
import type { EventSink, Fingerprinter, IdSource, ModelPort, ToolPort } from "./ports.js";
|
|
3
3
|
import { type HarnessLimits, type RunResult, type TaskInput } from "./types.js";
|
|
4
|
-
export declare const HARNESS_VERSION = "0.1.
|
|
4
|
+
export declare const HARNESS_VERSION = "0.1.2";
|
|
5
5
|
export interface AgentConfig {
|
|
6
6
|
readonly model: string;
|
|
7
7
|
readonly workingDirectory: string;
|
package/dist/harness/session.js
CHANGED
|
@@ -10,7 +10,7 @@ import { runLoop } from "./loop.js";
|
|
|
10
10
|
import { MemoryEventSink } from "./sinks.js";
|
|
11
11
|
import { resolveTaskPlan } from "./tasks/policy.js";
|
|
12
12
|
import { DEFAULT_LIMITS, } from "./types.js";
|
|
13
|
-
export const HARNESS_VERSION = "0.1.
|
|
13
|
+
export const HARNESS_VERSION = "0.1.2";
|
|
14
14
|
function resolveLimits(config) {
|
|
15
15
|
return { ...DEFAULT_LIMITS, ...config.limits };
|
|
16
16
|
}
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "0.1.
|
|
1
|
+
export declare const SDK_VERSION = "0.1.2";
|
|
2
2
|
export { createSession, type AgentConfig, type AgentSession, type HarnessDeps, type RunResult, type TaskInput, type TaskType, } from "../harness/index.js";
|
|
3
3
|
export { runAgent, type SdkAgentConfig, type SdkEvidenceOptions } from "./run-agent.js";
|
|
4
4
|
export { buildWorkspaceSummary, detectWorkspace, summarizeForAudit, type AuditEntry, type AuditSummary, type ContextEntrySummary, type ContextPackSummary, type WorkspaceInfo, type WorkspaceSummary, } from "../workspace/index.js";
|
package/dist/sdk/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Single-sourced package version; CLI and SDK both read this to avoid drift.
|
|
2
|
-
export const SDK_VERSION = "0.1.
|
|
2
|
+
export const SDK_VERSION = "0.1.2";
|
|
3
3
|
// The typed agent surface. AgentConfig, the session factory, the run result, and the
|
|
4
4
|
// session handle all live in the harness module (ADR-0004); the SDK re-exports them so
|
|
5
5
|
// callers import the agent API from one place.
|
package/dist/ui/csp-hashes.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
[
|
|
2
|
-
"'sha256
|
|
3
|
-
"'sha256-
|
|
2
|
+
"'sha256-/XgSSpwWKLLBpmcNR5yfmhouC89Mx4saOeSdHGSlLwA='",
|
|
3
|
+
"'sha256-6JBR+C4qigE40tMbninopqoSguR9H/AhsfWgllRr6y0='",
|
|
4
|
+
"'sha256-9YWu7RzXwUVkhu2CKTX5ddtKfcMNvJTmDtfvbtgQVgs='",
|
|
4
5
|
"'sha256-FhLHRUQz4c4ntLU9VkfEesX7PnzNLENSe/16Hi523Kk='",
|
|
5
|
-
"'sha256-
|
|
6
|
+
"'sha256-IvVUe6R8n3tqnjQ4DFKHkJcpkhct7eqEN1v2+2uU1kY='",
|
|
6
7
|
"'sha256-NMmsYxPlvKu6BMNDUuiUA/0HWXXhODWSkUJ3CrerHAI='",
|
|
7
8
|
"'sha256-OBTN3RiyCV4Bq7dFqZ5a2pAXjnCcCYeTJMO2I/LYKeo='",
|
|
9
|
+
"'sha256-SyPf64vF7t1VW6A0fQWPUKI7QXw9/m+zX0if/2n+ibc='",
|
|
8
10
|
"'sha256-U9W+ZoRW19rf6ohEfUh2oSN8UmJ8mZjCoxp31AbEGYM='",
|
|
9
|
-
"'sha256-Xhv62fM7dwD82udSCiqrICSFGK4rIE9t+2k2y0fsEwg='",
|
|
10
11
|
"'sha256-bg+CWjI8RppcgHYH6RuW4z4OnLAUEUPDXRoYUo9Tyok='",
|
|
11
|
-
"'sha256-dkXQrDpCKNXpywUKLu04aGkL4/5JXS2LWCKQ806R0rU='",
|
|
12
|
-
"'sha256-o5xPG0ZoS77FlzyLEClBD+u6el5Y3HrgzBsy+JPyHx8='",
|
|
13
12
|
"'sha256-qBQ7RdQKJEJuW7Fj1MbGjDbF6lnRdfu+KV0V4A5MTRg='",
|
|
14
13
|
"'sha256-qjuzziE6xLU3Cras89VlShlRYHgYZuOxceXUDmuvClo='",
|
|
15
14
|
"'sha256-xLP5QIbvR88RAxDKoSWqs6CVxNIRu17hhr7S/Q6hlU0='",
|
|
16
|
-
"'sha256-xz80fPjhAczg/tByXnm3xfZrdAUWODPmQtD4solyj1c='"
|
|
15
|
+
"'sha256-xz80fPjhAczg/tByXnm3xfZrdAUWODPmQtD4solyj1c='",
|
|
16
|
+
"'sha256-ygPaPap9QVGIk2na6327APzhMLDCt5Hh0UV/LPlFWNc='"
|
|
17
17
|
]
|
package/dist/ui/gateway-setup.js
CHANGED
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname } from "node:path";
|
|
7
7
|
import { Gateway, createDefaultChatCapability, listConfiguredCapabilities, parseGatewayConfig, toSafeObject, } from "../gateway/index.js";
|
|
8
|
+
import { gatewayFetch } from "../gateway/http.js";
|
|
8
9
|
import { redact } from "../gateway/redaction.js";
|
|
9
10
|
import { errorBody } from "./routes.js";
|
|
10
11
|
const MAX_BODY_BYTES = 64_000;
|
|
11
|
-
const MAX_DISCOVERED_MODELS =
|
|
12
|
+
const MAX_DISCOVERED_MODELS = 100;
|
|
13
|
+
const DISCOVERED_MODEL_SMOKE_TIMEOUT_MS = 15_000;
|
|
14
|
+
const DEPLOYMENT_SMOKE_TIMEOUT_MS = 30_000;
|
|
15
|
+
const SETUP_SMOKE_CONCURRENCY = 4;
|
|
12
16
|
class BodyTooLargeError extends Error {
|
|
13
17
|
constructor() {
|
|
14
18
|
super("request body too large");
|
|
@@ -54,41 +58,80 @@ function normalizeBaseUrl(raw) {
|
|
|
54
58
|
function candidateBaseUrls(baseUrl) {
|
|
55
59
|
const primary = normalizeBaseUrl(baseUrl);
|
|
56
60
|
const candidates = [primary];
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
try {
|
|
62
|
+
const url = new URL(primary);
|
|
63
|
+
if (url.hostname.endsWith(".services.ai.azure.com")) {
|
|
64
|
+
if (url.pathname === "" || url.pathname === "/") {
|
|
65
|
+
candidates.push(`${url.origin}/openai/v1`);
|
|
66
|
+
}
|
|
67
|
+
else if (primary.endsWith("/openai")) {
|
|
68
|
+
candidates.push(`${primary}/v1`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (!primary.endsWith("/v1")) {
|
|
72
|
+
candidates.push(`${primary}/v1`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
if (!primary.endsWith("/v1")) {
|
|
77
|
+
candidates.push(`${primary}/v1`);
|
|
78
|
+
}
|
|
59
79
|
}
|
|
60
80
|
return Array.from(new Set(candidates));
|
|
61
81
|
}
|
|
62
|
-
function
|
|
82
|
+
function isAzureFoundryBaseUrl(baseUrl) {
|
|
83
|
+
try {
|
|
84
|
+
const url = new URL(baseUrl);
|
|
85
|
+
return url.hostname.endsWith(".services.ai.azure.com");
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function providerRaw(modelId, baseUrl, apiKey, options = {}) {
|
|
63
92
|
return {
|
|
64
93
|
modelId,
|
|
65
94
|
baseUrl,
|
|
66
95
|
apiKey,
|
|
67
96
|
capability: createDefaultChatCapability(modelId),
|
|
68
|
-
timeoutMs: 30_000,
|
|
69
|
-
maxRetries: 2,
|
|
97
|
+
timeoutMs: options.timeoutMs ?? 30_000,
|
|
98
|
+
maxRetries: options.maxRetries ?? 2,
|
|
70
99
|
retryBaseDelayMs: 500,
|
|
71
100
|
};
|
|
72
101
|
}
|
|
73
|
-
function buildRawConfig(baseUrl, apiKey, modelIds) {
|
|
102
|
+
function buildRawConfig(baseUrl, apiKey, modelIds, options = {}) {
|
|
74
103
|
return {
|
|
75
|
-
providers: modelIds.map((modelId) => providerRaw(modelId, baseUrl, apiKey)),
|
|
104
|
+
providers: modelIds.map((modelId) => providerRaw(modelId, baseUrl, apiKey, options)),
|
|
76
105
|
circuitBreaker: { failureThreshold: 5, cooldownMs: 30_000, halfOpenProbes: 2 },
|
|
77
106
|
};
|
|
78
107
|
}
|
|
79
108
|
function modelsEndpoint(baseUrl) {
|
|
80
109
|
return `${baseUrl}/models`;
|
|
81
110
|
}
|
|
111
|
+
function modelIdFromDiscoveryItem(item) {
|
|
112
|
+
if (!isRecord(item) || typeof item.id !== "string") {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
const id = item.id.trim();
|
|
116
|
+
if (id.length === 0) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
const capabilities = isRecord(item.capabilities) ? item.capabilities : undefined;
|
|
120
|
+
if (capabilities?.chat_completion === false) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
return id;
|
|
124
|
+
}
|
|
82
125
|
function parseModelList(payload) {
|
|
83
126
|
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
84
127
|
throw new Error("model discovery response must contain a data array");
|
|
85
128
|
}
|
|
86
129
|
const ids = [];
|
|
87
130
|
for (const item of payload.data) {
|
|
88
|
-
|
|
89
|
-
|
|
131
|
+
const id = modelIdFromDiscoveryItem(item);
|
|
132
|
+
if (id !== undefined) {
|
|
133
|
+
ids.push(id);
|
|
90
134
|
}
|
|
91
|
-
ids.push(item.id.trim());
|
|
92
135
|
}
|
|
93
136
|
const unique = Array.from(new Set(ids));
|
|
94
137
|
if (unique.length === 0) {
|
|
@@ -97,7 +140,7 @@ function parseModelList(payload) {
|
|
|
97
140
|
return unique.slice(0, MAX_DISCOVERED_MODELS);
|
|
98
141
|
}
|
|
99
142
|
async function defaultGatewayModelDiscovery(baseUrl, apiKey) {
|
|
100
|
-
const response = await
|
|
143
|
+
const response = await gatewayFetch(modelsEndpoint(baseUrl), {
|
|
101
144
|
method: "GET",
|
|
102
145
|
headers: { authorization: `Bearer ${apiKey}` },
|
|
103
146
|
signal: AbortSignal.timeout(30_000),
|
|
@@ -114,26 +157,62 @@ async function defaultGatewayModelDiscovery(baseUrl, apiKey) {
|
|
|
114
157
|
}
|
|
115
158
|
return parseModelList(payload);
|
|
116
159
|
}
|
|
160
|
+
function deploymentNameValues(value) {
|
|
161
|
+
if (typeof value === "string") {
|
|
162
|
+
return value.split(/[\n,]/u);
|
|
163
|
+
}
|
|
164
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
function normalizeDeploymentNames(values) {
|
|
170
|
+
return Array.from(new Set(values.map((item) => item.trim()).filter((item) => item.length > 0)));
|
|
171
|
+
}
|
|
172
|
+
function parseDeploymentNames(value) {
|
|
173
|
+
if (value === undefined) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
const values = deploymentNameValues(value);
|
|
177
|
+
if (values === undefined) {
|
|
178
|
+
return {
|
|
179
|
+
status: 400,
|
|
180
|
+
body: errorBody("BAD_REQUEST", "deploymentNames must be a string or an array of strings."),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return normalizeDeploymentNames(values);
|
|
184
|
+
}
|
|
117
185
|
async function defaultGatewaySetupTester(config, candidateModelIds) {
|
|
118
186
|
const gateway = new Gateway(config);
|
|
119
|
-
const tested =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
187
|
+
const tested = Array(candidateModelIds.length).fill(undefined);
|
|
188
|
+
let next = 0;
|
|
189
|
+
async function worker() {
|
|
190
|
+
while (next < candidateModelIds.length) {
|
|
191
|
+
const index = next;
|
|
192
|
+
next += 1;
|
|
193
|
+
const modelId = candidateModelIds[index];
|
|
194
|
+
if (modelId === undefined) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
await gateway.chat({
|
|
199
|
+
modelId,
|
|
200
|
+
messages: [{ role: "user", content: "Reply with exactly: OK" }],
|
|
201
|
+
});
|
|
202
|
+
tested[index] = modelId;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// Non-chat models can appear in OpenAI-compatible model discovery responses. They are
|
|
206
|
+
// intentionally ignored so only chat-callable models become selectable in the UI.
|
|
207
|
+
}
|
|
131
208
|
}
|
|
132
209
|
}
|
|
133
|
-
|
|
210
|
+
await Promise.all(Array.from({ length: Math.min(SETUP_SMOKE_CONCURRENCY, candidateModelIds.length) }, () => worker()));
|
|
211
|
+
const accepted = tested.filter((modelId) => modelId !== undefined);
|
|
212
|
+
if (accepted.length === 0) {
|
|
134
213
|
throw new Error("no discovered model accepted the chat-completions smoke test");
|
|
135
214
|
}
|
|
136
|
-
return
|
|
215
|
+
return accepted;
|
|
137
216
|
}
|
|
138
217
|
function savePrivateJson(path, raw) {
|
|
139
218
|
const dir = dirname(path);
|
|
@@ -155,7 +234,11 @@ function readSetupRequest(raw) {
|
|
|
155
234
|
if (baseUrl.length === 0 || apiKey.length === 0) {
|
|
156
235
|
return { status: 400, body: errorBody("BAD_REQUEST", "baseUrl and apiKey are required.") };
|
|
157
236
|
}
|
|
158
|
-
|
|
237
|
+
const deploymentNames = parseDeploymentNames(raw.deploymentNames);
|
|
238
|
+
if ("status" in deploymentNames) {
|
|
239
|
+
return deploymentNames;
|
|
240
|
+
}
|
|
241
|
+
return { baseUrl, apiKey, deploymentNames };
|
|
159
242
|
}
|
|
160
243
|
function safeError(error, secrets) {
|
|
161
244
|
if (error instanceof Error) {
|
|
@@ -163,9 +246,13 @@ function safeError(error, secrets) {
|
|
|
163
246
|
}
|
|
164
247
|
return "Gateway setup failed.";
|
|
165
248
|
}
|
|
166
|
-
async function verifySetupCandidate(baseUrl, apiKey, tester, discovery) {
|
|
167
|
-
const candidateModelIds = await discovery(baseUrl, apiKey);
|
|
168
|
-
const
|
|
249
|
+
async function verifySetupCandidate(baseUrl, apiKey, deploymentNames, tester, discovery) {
|
|
250
|
+
const candidateModelIds = deploymentNames.length > 0 ? deploymentNames : await discovery(baseUrl, apiKey);
|
|
251
|
+
const smokeTimeoutMs = deploymentNames.length > 0 ? DEPLOYMENT_SMOKE_TIMEOUT_MS : DISCOVERED_MODEL_SMOKE_TIMEOUT_MS;
|
|
252
|
+
const candidateRawConfig = buildRawConfig(baseUrl, apiKey, candidateModelIds, {
|
|
253
|
+
timeoutMs: smokeTimeoutMs,
|
|
254
|
+
maxRetries: 0,
|
|
255
|
+
});
|
|
169
256
|
const candidateConfig = parseGatewayConfig(candidateRawConfig);
|
|
170
257
|
const testedModelIds = await tester(candidateConfig, candidateModelIds);
|
|
171
258
|
const rawConfig = buildRawConfig(baseUrl, apiKey, testedModelIds);
|
|
@@ -192,10 +279,13 @@ function setupFailureResult(errors) {
|
|
|
192
279
|
body: errorBody("GATEWAY_SETUP_FAILED", `Credentials could not be verified. ${errors.join(" ")}`),
|
|
193
280
|
};
|
|
194
281
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
282
|
+
function deploymentNamesRequiredResult() {
|
|
283
|
+
return {
|
|
284
|
+
status: 400,
|
|
285
|
+
body: errorBody("GATEWAY_DEPLOYMENTS_REQUIRED", "Azure AI Foundry endpoints require deployment names from the Deployments tab."),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
async function readJsonSetupBody(ctx) {
|
|
199
289
|
let bodyText;
|
|
200
290
|
try {
|
|
201
291
|
bodyText = await readBody(ctx.req);
|
|
@@ -206,23 +296,42 @@ export async function handleGatewaySetup(ctx, deps) {
|
|
|
206
296
|
}
|
|
207
297
|
throw error;
|
|
208
298
|
}
|
|
209
|
-
let parsed;
|
|
210
299
|
try {
|
|
211
|
-
parsed
|
|
300
|
+
return { parsed: JSON.parse(bodyText) };
|
|
212
301
|
}
|
|
213
302
|
catch {
|
|
214
303
|
return { status: 400, body: errorBody("BAD_REQUEST", "Request body is not valid JSON.") };
|
|
215
304
|
}
|
|
216
|
-
|
|
305
|
+
}
|
|
306
|
+
function gatewayUnavailableResult() {
|
|
307
|
+
return {
|
|
308
|
+
status: 500,
|
|
309
|
+
body: errorBody("GATEWAY_SETUP_UNAVAILABLE", "Gateway setup is unavailable."),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
export async function handleGatewaySetup(ctx, deps) {
|
|
313
|
+
if (deps.gatewayConfig === undefined) {
|
|
314
|
+
return gatewayUnavailableResult();
|
|
315
|
+
}
|
|
316
|
+
const bodyResult = await readJsonSetupBody(ctx);
|
|
317
|
+
if ("status" in bodyResult) {
|
|
318
|
+
return bodyResult;
|
|
319
|
+
}
|
|
320
|
+
const request = readSetupRequest(bodyResult.parsed);
|
|
217
321
|
if ("status" in request) {
|
|
218
322
|
return request;
|
|
219
323
|
}
|
|
220
324
|
const tester = deps.gatewaySetupTester ?? defaultGatewaySetupTester;
|
|
221
325
|
const discovery = deps.gatewayModelDiscovery ?? defaultGatewayModelDiscovery;
|
|
326
|
+
const baseUrlCandidates = candidateBaseUrls(request.baseUrl);
|
|
327
|
+
if (request.deploymentNames.length === 0 &&
|
|
328
|
+
baseUrlCandidates.some((baseUrl) => isAzureFoundryBaseUrl(baseUrl))) {
|
|
329
|
+
return deploymentNamesRequiredResult();
|
|
330
|
+
}
|
|
222
331
|
const errors = [];
|
|
223
|
-
for (const baseUrl of
|
|
332
|
+
for (const baseUrl of baseUrlCandidates) {
|
|
224
333
|
try {
|
|
225
|
-
const verified = await verifySetupCandidate(baseUrl, request.apiKey, tester, discovery);
|
|
334
|
+
const verified = await verifySetupCandidate(baseUrl, request.apiKey, request.deploymentNames, tester, discovery);
|
|
226
335
|
savePrivateJson(deps.gatewayConfig.storagePath, verified.rawConfig);
|
|
227
336
|
deps.gatewayConfig.set(verified.config, true);
|
|
228
337
|
return setupSuccessResult(verified.config, verified.testedModelIds);
|
package/dist/ui/static/404.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE html><!--
|
|
1
|
+
<!DOCTYPE html><!--pBbsB1CYvKAPHJwR_PXZP--><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\":\"pBbsB1CYvKAPHJwR_PXZP\",\"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>
|