@oscharko-dev/keiko 0.1.1 → 0.1.3

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,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 this.fetchImpl(url, {
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.1";
4
+ export declare const HARNESS_VERSION = "0.1.3";
5
5
  export interface AgentConfig {
6
6
  readonly model: string;
7
7
  readonly workingDirectory: string;
@@ -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.1";
13
+ export const HARNESS_VERSION = "0.1.3";
14
14
  function resolveLimits(config) {
15
15
  return { ...DEFAULT_LIMITS, ...config.limits };
16
16
  }
@@ -1,4 +1,4 @@
1
- export declare const SDK_VERSION = "0.1.1";
1
+ export declare const SDK_VERSION = "0.1.3";
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.1";
2
+ export const SDK_VERSION = "0.1.3";
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.
@@ -1,17 +1,17 @@
1
1
  [
2
- "'sha256-1iAwQ7tnZDg5wmyk5hnRjp7fgGRgzmKarL9cyjgbjCU='",
3
- "'sha256-6/dVx3zDc4W3r0O17p0axKInzbrfnxtLRLJBFSWJPCA='",
2
+ "'sha256-6JBR+C4qigE40tMbninopqoSguR9H/AhsfWgllRr6y0='",
3
+ "'sha256-DSZruQ2vcS+6pG958yMjEei/9gtD6NyThy7uKiGoiyg='",
4
4
  "'sha256-FhLHRUQz4c4ntLU9VkfEesX7PnzNLENSe/16Hi523Kk='",
5
- "'sha256-JvBgydXt9/FYJ748ftCVGdeG0nrCETPaVqZP4+fgvkQ='",
6
5
  "'sha256-NMmsYxPlvKu6BMNDUuiUA/0HWXXhODWSkUJ3CrerHAI='",
7
6
  "'sha256-OBTN3RiyCV4Bq7dFqZ5a2pAXjnCcCYeTJMO2I/LYKeo='",
7
+ "'sha256-PbnZrJeiluw2LIBQCiPtFtG9jmUCwypFCLKHbgBtUlw='",
8
8
  "'sha256-U9W+ZoRW19rf6ohEfUh2oSN8UmJ8mZjCoxp31AbEGYM='",
9
+ "'sha256-ZyoCerHb/k3dCxBy25xejgU3nfPTwNTF4yBHXaS2sns='",
9
10
  "'sha256-bg+CWjI8RppcgHYH6RuW4z4OnLAUEUPDXRoYUo9Tyok='",
10
- "'sha256-dkXQrDpCKNXpywUKLu04aGkL4/5JXS2LWCKQ806R0rU='",
11
- "'sha256-o5xPG0ZoS77FlzyLEClBD+u6el5Y3HrgzBsy+JPyHx8='",
11
+ "'sha256-eUSdg12DtpaM7G/VBVS226GbsPqkzkyalIiXO6diQS8='",
12
12
  "'sha256-qBQ7RdQKJEJuW7Fj1MbGjDbF6lnRdfu+KV0V4A5MTRg='",
13
+ "'sha256-qFjHb7sY6bZLxg005pqG16u4FRF/5pjgtIEUIUQjJO0='",
13
14
  "'sha256-qjuzziE6xLU3Cras89VlShlRYHgYZuOxceXUDmuvClo='",
14
- "'sha256-tNBStdeJtAUzvvVIXq46ljXN4B//G94woiyzjSzQ7TY='",
15
15
  "'sha256-xLP5QIbvR88RAxDKoSWqs6CVxNIRu17hhr7S/Q6hlU0='",
16
16
  "'sha256-xz80fPjhAczg/tByXnm3xfZrdAUWODPmQtD4solyj1c='"
17
17
  ]
@@ -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 = 25;
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
- if (!primary.endsWith("/v1")) {
58
- candidates.push(`${primary}/v1`);
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 providerRaw(modelId, baseUrl, apiKey) {
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
- if (!isRecord(item) || typeof item.id !== "string" || item.id.trim().length === 0) {
89
- continue;
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 fetch(modelsEndpoint(baseUrl), {
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
- for (const modelId of candidateModelIds) {
121
- try {
122
- await gateway.chat({
123
- modelId,
124
- messages: [{ role: "user", content: "Reply with exactly: OK" }],
125
- });
126
- tested.push(modelId);
127
- }
128
- catch {
129
- // Non-chat models can appear in OpenAI-compatible model discovery responses. They are
130
- // intentionally ignored so only chat-callable models become selectable in the UI.
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
- if (tested.length === 0) {
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 tested;
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
- return { baseUrl, apiKey };
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 candidateRawConfig = buildRawConfig(baseUrl, apiKey, candidateModelIds);
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
- export async function handleGatewaySetup(ctx, deps) {
196
- if (deps.gatewayConfig === undefined) {
197
- return { status: 500, body: errorBody("GATEWAY_SETUP_UNAVAILABLE", "Gateway setup is unavailable.") };
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 = JSON.parse(bodyText);
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
- const request = readSetupRequest(parsed);
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 candidateBaseUrls(request.baseUrl)) {
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);
@@ -1 +1 @@
1
- <!DOCTYPE html><!--8NJpsI_ZXw2ixaYi18bZI--><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/3d68155c8db012f4.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/3d68155c8db012f4.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"8NJpsI-ZXw2ixaYi18bZI\",\"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/3d68155c8db012f4.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><!--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>