llm-cli-gateway 2.10.0 → 2.11.0
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/CHANGELOG.md +52 -0
- package/README.md +7 -5
- package/dist/acp/event-normalizer.d.ts +42 -0
- package/dist/acp/event-normalizer.js +71 -0
- package/dist/acp/flight-redaction.d.ts +25 -0
- package/dist/acp/flight-redaction.js +40 -0
- package/dist/acp/host-services.d.ts +16 -0
- package/dist/acp/host-services.js +29 -0
- package/dist/acp/permission-bridge.d.ts +15 -0
- package/dist/acp/permission-bridge.js +90 -0
- package/dist/acp/process-manager.js +7 -1
- package/dist/acp/provider-registry.d.ts +1 -1
- package/dist/acp/provider-registry.js +13 -0
- package/dist/acp/runtime.d.ts +35 -0
- package/dist/acp/runtime.js +125 -0
- package/dist/acp/session-map.d.ts +42 -0
- package/dist/acp/session-map.js +67 -0
- package/dist/acp/smoke-harness.d.ts +28 -0
- package/dist/acp/smoke-harness.js +90 -0
- package/dist/api-http.d.ts +18 -0
- package/dist/api-http.js +122 -0
- package/dist/api-provider.d.ts +83 -0
- package/dist/api-provider.js +258 -0
- package/dist/api-request.d.ts +30 -0
- package/dist/api-request.js +51 -0
- package/dist/approval-manager.d.ts +1 -1
- package/dist/approval-manager.js +6 -7
- package/dist/async-job-manager.d.ts +19 -4
- package/dist/async-job-manager.js +211 -35
- package/dist/claude-mcp-config.d.ts +2 -2
- package/dist/claude-mcp-config.js +42 -52
- package/dist/cli-updater.js +16 -1
- package/dist/config.d.ts +20 -0
- package/dist/config.js +93 -35
- package/dist/doctor.d.ts +1 -1
- package/dist/flight-recorder.d.ts +1 -0
- package/dist/flight-recorder.js +11 -0
- package/dist/index.d.ts +56 -5
- package/dist/index.js +639 -38
- package/dist/job-store.d.ts +15 -0
- package/dist/job-store.js +39 -5
- package/dist/mcp-registry.d.ts +17 -0
- package/dist/mcp-registry.js +5 -0
- package/dist/metrics.js +7 -2
- package/dist/model-registry.js +11 -0
- package/dist/prompt-parts.d.ts +6 -6
- package/dist/provider-login-guidance.js +21 -0
- package/dist/provider-status.js +4 -1
- package/dist/provider-tool-capabilities.d.ts +4 -3
- package/dist/provider-tool-capabilities.js +93 -6
- package/dist/request-helpers.d.ts +6 -6
- package/dist/request-helpers.js +1 -4
- package/dist/session-manager-pg.js +2 -9
- package/dist/session-manager.d.ts +9 -4
- package/dist/session-manager.js +13 -4
- package/dist/upstream-contracts.js +112 -2
- package/dist/validation-normalizer.d.ts +2 -2
- package/dist/validation-orchestrator.d.ts +2 -0
- package/dist/validation-orchestrator.js +28 -7
- package/dist/validation-tools.d.ts +61 -0
- package/dist/validation-tools.js +36 -21
- package/migrations/005_provider_type_open_api_names.sql +28 -0
- package/npm-shrinkwrap.json +4 -3
- package/package.json +12 -9
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CliType, ISessionManager } from "../session-manager.js";
|
|
2
|
+
export declare const ACP_TRANSPORT: "acp";
|
|
3
|
+
export interface AcpSessionMetadata {
|
|
4
|
+
readonly provider: CliType;
|
|
5
|
+
readonly transport: typeof ACP_TRANSPORT;
|
|
6
|
+
readonly sessionId?: string;
|
|
7
|
+
readonly protocolVersion?: number;
|
|
8
|
+
readonly agentName?: string;
|
|
9
|
+
readonly agentVersion?: string;
|
|
10
|
+
readonly cwd?: string;
|
|
11
|
+
readonly workspaceAlias?: string;
|
|
12
|
+
readonly worktreePath?: string;
|
|
13
|
+
readonly createdAt: string;
|
|
14
|
+
readonly lastSeenAt: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function isGatewaySessionId(id: string): boolean;
|
|
17
|
+
export declare function newGatewaySessionId(): string;
|
|
18
|
+
export interface CreateAcpSessionParams {
|
|
19
|
+
readonly provider: CliType;
|
|
20
|
+
readonly cwd?: string;
|
|
21
|
+
readonly workspaceAlias?: string;
|
|
22
|
+
readonly worktreePath?: string;
|
|
23
|
+
readonly now?: () => string;
|
|
24
|
+
}
|
|
25
|
+
export declare function createAcpSession(sessionManager: ISessionManager, params: CreateAcpSessionParams): Promise<string>;
|
|
26
|
+
export interface AcpSessionInfo {
|
|
27
|
+
readonly providerSessionId: string;
|
|
28
|
+
readonly protocolVersion?: number;
|
|
29
|
+
readonly agentName?: string;
|
|
30
|
+
readonly agentVersion?: string;
|
|
31
|
+
readonly now?: () => string;
|
|
32
|
+
}
|
|
33
|
+
export declare function recordAcpSessionInfo(sessionManager: ISessionManager, gatewaySessionId: string, info: AcpSessionInfo): Promise<boolean>;
|
|
34
|
+
export type AcpResumeResult = {
|
|
35
|
+
readonly ok: true;
|
|
36
|
+
readonly providerSessionId: string;
|
|
37
|
+
readonly metadata: AcpSessionMetadata;
|
|
38
|
+
} | {
|
|
39
|
+
readonly ok: false;
|
|
40
|
+
readonly reason: "not_found" | "wrong_provider" | "wrong_transport" | "no_provider_session";
|
|
41
|
+
};
|
|
42
|
+
export declare function resolveAcpResume(sessionManager: ISessionManager, gatewaySessionId: string, provider: CliType): Promise<AcpResumeResult>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { GATEWAY_SESSION_PREFIX } from "../request-helpers.js";
|
|
3
|
+
export const ACP_TRANSPORT = "acp";
|
|
4
|
+
export function isGatewaySessionId(id) {
|
|
5
|
+
return id.startsWith(GATEWAY_SESSION_PREFIX);
|
|
6
|
+
}
|
|
7
|
+
export function newGatewaySessionId() {
|
|
8
|
+
return `${GATEWAY_SESSION_PREFIX}${randomUUID()}`;
|
|
9
|
+
}
|
|
10
|
+
export async function createAcpSession(sessionManager, params) {
|
|
11
|
+
const now = (params.now ?? (() => new Date().toISOString()))();
|
|
12
|
+
const gatewaySessionId = newGatewaySessionId();
|
|
13
|
+
const metadata = {
|
|
14
|
+
provider: params.provider,
|
|
15
|
+
transport: ACP_TRANSPORT,
|
|
16
|
+
cwd: params.cwd,
|
|
17
|
+
workspaceAlias: params.workspaceAlias,
|
|
18
|
+
worktreePath: params.worktreePath,
|
|
19
|
+
createdAt: now,
|
|
20
|
+
lastSeenAt: now,
|
|
21
|
+
};
|
|
22
|
+
await sessionManager.createSession(params.provider, `${params.provider} ACP Session`, gatewaySessionId);
|
|
23
|
+
const stamped = await sessionManager.updateSessionMetadata(gatewaySessionId, { acp: metadata });
|
|
24
|
+
if (!stamped) {
|
|
25
|
+
throw new Error("failed to stamp ACP metadata on the new gateway session");
|
|
26
|
+
}
|
|
27
|
+
return gatewaySessionId;
|
|
28
|
+
}
|
|
29
|
+
export async function recordAcpSessionInfo(sessionManager, gatewaySessionId, info) {
|
|
30
|
+
const existing = await readAcpMetadata(sessionManager, gatewaySessionId);
|
|
31
|
+
if (!existing)
|
|
32
|
+
return false;
|
|
33
|
+
const now = (info.now ?? (() => new Date().toISOString()))();
|
|
34
|
+
const merged = {
|
|
35
|
+
...existing,
|
|
36
|
+
sessionId: info.providerSessionId,
|
|
37
|
+
protocolVersion: info.protocolVersion ?? existing.protocolVersion,
|
|
38
|
+
agentName: info.agentName ?? existing.agentName,
|
|
39
|
+
agentVersion: info.agentVersion ?? existing.agentVersion,
|
|
40
|
+
lastSeenAt: now,
|
|
41
|
+
};
|
|
42
|
+
return sessionManager.updateSessionMetadata(gatewaySessionId, { acp: merged });
|
|
43
|
+
}
|
|
44
|
+
export async function resolveAcpResume(sessionManager, gatewaySessionId, provider) {
|
|
45
|
+
const acp = await readAcpMetadata(sessionManager, gatewaySessionId);
|
|
46
|
+
if (!acp) {
|
|
47
|
+
const session = await sessionManager.getSession(gatewaySessionId);
|
|
48
|
+
return { ok: false, reason: session ? "wrong_transport" : "not_found" };
|
|
49
|
+
}
|
|
50
|
+
if (acp.transport !== ACP_TRANSPORT) {
|
|
51
|
+
return { ok: false, reason: "wrong_transport" };
|
|
52
|
+
}
|
|
53
|
+
if (acp.provider !== provider) {
|
|
54
|
+
return { ok: false, reason: "wrong_provider" };
|
|
55
|
+
}
|
|
56
|
+
if (!acp.sessionId) {
|
|
57
|
+
return { ok: false, reason: "no_provider_session" };
|
|
58
|
+
}
|
|
59
|
+
return { ok: true, providerSessionId: acp.sessionId, metadata: acp };
|
|
60
|
+
}
|
|
61
|
+
async function readAcpMetadata(sessionManager, gatewaySessionId) {
|
|
62
|
+
const session = await sessionManager.getSession(gatewaySessionId);
|
|
63
|
+
const acp = session?.metadata?.acp;
|
|
64
|
+
if (!acp || typeof acp !== "object")
|
|
65
|
+
return null;
|
|
66
|
+
return acp;
|
|
67
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type AcpSpawnFn, type ProcessEnv } from "./process-manager.js";
|
|
2
|
+
import type { AcpConfig } from "../config.js";
|
|
3
|
+
import type { Logger } from "../logger.js";
|
|
4
|
+
import type { CliType } from "../session-manager.js";
|
|
5
|
+
export interface AcpSmokeResult {
|
|
6
|
+
readonly provider: CliType;
|
|
7
|
+
readonly ok: boolean;
|
|
8
|
+
readonly protocolVersion: number | null;
|
|
9
|
+
readonly agentName: string | null;
|
|
10
|
+
readonly agentVersion: string | null;
|
|
11
|
+
readonly sessionCreated: boolean;
|
|
12
|
+
readonly durationMs: number;
|
|
13
|
+
readonly error: {
|
|
14
|
+
readonly kind: string;
|
|
15
|
+
readonly message: string;
|
|
16
|
+
} | null;
|
|
17
|
+
}
|
|
18
|
+
export interface AcpSmokeOptions {
|
|
19
|
+
readonly config: AcpConfig;
|
|
20
|
+
readonly logger?: Logger;
|
|
21
|
+
readonly spawn?: AcpSpawnFn;
|
|
22
|
+
readonly baseEnv?: ProcessEnv;
|
|
23
|
+
readonly cwd?: string;
|
|
24
|
+
readonly now?: () => number;
|
|
25
|
+
}
|
|
26
|
+
export declare function runAcpSmoke(provider: CliType, options: AcpSmokeOptions): Promise<AcpSmokeResult>;
|
|
27
|
+
export declare function runAcpSmokes(providers: readonly CliType[], options: AcpSmokeOptions): Promise<AcpSmokeResult[]>;
|
|
28
|
+
export declare function eligibleSmokeProviders(config: AcpConfig): CliType[];
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
2
|
+
import { AcpError } from "./errors.js";
|
|
3
|
+
import { AcpProcessManager } from "./process-manager.js";
|
|
4
|
+
import { getAcpProviderEntry, providerHasNativeAcp } from "./provider-registry.js";
|
|
5
|
+
import { noopLogger } from "../logger.js";
|
|
6
|
+
const SMOKE_HOST_SERVICES = Object.freeze({});
|
|
7
|
+
export async function runAcpSmoke(provider, options) {
|
|
8
|
+
const logger = options.logger ?? noopLogger;
|
|
9
|
+
const now = options.now ?? Date.now;
|
|
10
|
+
const startedAt = now();
|
|
11
|
+
const elapsed = () => Math.max(0, now() - startedAt);
|
|
12
|
+
if (!providerHasNativeAcp(provider)) {
|
|
13
|
+
return {
|
|
14
|
+
provider,
|
|
15
|
+
ok: false,
|
|
16
|
+
protocolVersion: null,
|
|
17
|
+
agentName: null,
|
|
18
|
+
agentVersion: null,
|
|
19
|
+
sessionCreated: false,
|
|
20
|
+
durationMs: elapsed(),
|
|
21
|
+
error: {
|
|
22
|
+
kind: "provider_unavailable",
|
|
23
|
+
message: `${getAcpProviderEntry(provider).displayName} has no native ACP entrypoint.`,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const manager = new AcpProcessManager({
|
|
28
|
+
config: options.config,
|
|
29
|
+
logger,
|
|
30
|
+
spawn: options.spawn,
|
|
31
|
+
baseEnv: options.baseEnv,
|
|
32
|
+
});
|
|
33
|
+
const cwd = options.cwd ?? `${tmpdir()}/llm-gateway-acp-smoke-${provider}`;
|
|
34
|
+
try {
|
|
35
|
+
const proc = await manager.start({ provider, cwd, hostServices: SMOKE_HOST_SERVICES });
|
|
36
|
+
try {
|
|
37
|
+
const init = proc.client.agentInfo;
|
|
38
|
+
const session = await proc.client.newSession({ cwd, mcpServers: [] });
|
|
39
|
+
logger.info("acp.smoke.success", {
|
|
40
|
+
provider,
|
|
41
|
+
protocolVersion: init?.protocolVersion,
|
|
42
|
+
durationMs: elapsed(),
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
provider,
|
|
46
|
+
ok: true,
|
|
47
|
+
protocolVersion: init?.protocolVersion ?? null,
|
|
48
|
+
agentName: init?.agentInfo?.name ?? null,
|
|
49
|
+
agentVersion: init?.agentInfo?.version ?? null,
|
|
50
|
+
sessionCreated: session.sessionId.length > 0,
|
|
51
|
+
durationMs: elapsed(),
|
|
52
|
+
error: null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
proc.shutdown("SIGTERM");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const kind = err instanceof AcpError ? err.kind : "unknown";
|
|
61
|
+
const message = err instanceof AcpError ? err.userMessage : `ACP smoke for ${provider} failed unexpectedly.`;
|
|
62
|
+
logger.error("acp.smoke.failure", { provider, kind, durationMs: elapsed() });
|
|
63
|
+
return {
|
|
64
|
+
provider,
|
|
65
|
+
ok: false,
|
|
66
|
+
protocolVersion: null,
|
|
67
|
+
agentName: null,
|
|
68
|
+
agentVersion: null,
|
|
69
|
+
sessionCreated: false,
|
|
70
|
+
durationMs: elapsed(),
|
|
71
|
+
error: { kind, message },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
manager.shutdownAll("SIGKILL");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function runAcpSmokes(providers, options) {
|
|
79
|
+
const results = [];
|
|
80
|
+
for (const provider of providers) {
|
|
81
|
+
results.push(await runAcpSmoke(provider, options));
|
|
82
|
+
}
|
|
83
|
+
return results;
|
|
84
|
+
}
|
|
85
|
+
export function eligibleSmokeProviders(config) {
|
|
86
|
+
if (!config.enabled) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
return Object.keys(config.providers).filter(provider => providerHasNativeAcp(provider) && config.providers[provider]?.enabled === true);
|
|
90
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { URL } from "node:url";
|
|
2
|
+
export declare const MAX_API_RESPONSE_BYTES: number;
|
|
3
|
+
export declare const DEFAULT_API_TIMEOUT_MS = 600000;
|
|
4
|
+
export declare class ApiHttpError extends Error {
|
|
5
|
+
readonly status: number | null;
|
|
6
|
+
readonly responseText: string;
|
|
7
|
+
readonly code?: string | undefined;
|
|
8
|
+
constructor(message: string, status?: number | null, responseText?: string, code?: string | undefined);
|
|
9
|
+
}
|
|
10
|
+
export declare function isHttpsOrLoopbackUrl(value: string): boolean;
|
|
11
|
+
export declare function isLoopbackUrl(value: string): boolean;
|
|
12
|
+
export declare function buildEndpointUrl(baseUrl: string, path: string): URL;
|
|
13
|
+
export declare function isHttpTransient(error: unknown): boolean;
|
|
14
|
+
export interface ApiHttpResponse {
|
|
15
|
+
status: number;
|
|
16
|
+
text: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function postJson(url: URL, body: unknown, headers: Record<string, string>, timeoutMs: number, extractErrorMessage?: (status: number, responseBody: string) => string, signal?: AbortSignal): Promise<ApiHttpResponse>;
|
package/dist/api-http.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { request as httpRequest } from "node:http";
|
|
2
|
+
import { request as httpsRequest } from "node:https";
|
|
3
|
+
import { URL } from "node:url";
|
|
4
|
+
export const MAX_API_RESPONSE_BYTES = 50 * 1024 * 1024;
|
|
5
|
+
export const DEFAULT_API_TIMEOUT_MS = 600_000;
|
|
6
|
+
const LOOPBACK_HOSTS = ["localhost", "127.0.0.1", "::1", "[::1]"];
|
|
7
|
+
export class ApiHttpError extends Error {
|
|
8
|
+
status;
|
|
9
|
+
responseText;
|
|
10
|
+
code;
|
|
11
|
+
constructor(message, status = null, responseText = "", code) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.responseText = responseText;
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.name = "ApiHttpError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function isHttpsOrLoopbackUrl(value) {
|
|
20
|
+
try {
|
|
21
|
+
const url = new URL(value);
|
|
22
|
+
if (url.protocol === "https:")
|
|
23
|
+
return true;
|
|
24
|
+
if (url.protocol !== "http:")
|
|
25
|
+
return false;
|
|
26
|
+
return LOOPBACK_HOSTS.includes(url.hostname);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function isLoopbackUrl(value) {
|
|
33
|
+
try {
|
|
34
|
+
const url = new URL(value);
|
|
35
|
+
return ((url.protocol === "http:" || url.protocol === "https:") &&
|
|
36
|
+
LOOPBACK_HOSTS.includes(url.hostname));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function buildEndpointUrl(baseUrl, path) {
|
|
43
|
+
const trimmedBase = baseUrl.replace(/\/+$/, "");
|
|
44
|
+
const trimmedPath = path.replace(/^\/+/, "");
|
|
45
|
+
const url = new URL(`${trimmedBase}/${trimmedPath}`);
|
|
46
|
+
if (url.protocol !== "https:" &&
|
|
47
|
+
!(url.protocol === "http:" && LOOPBACK_HOSTS.includes(url.hostname))) {
|
|
48
|
+
throw new ApiHttpError(`API base_url must use https unless it targets localhost/loopback (got ${url.protocol}//${url.hostname})`);
|
|
49
|
+
}
|
|
50
|
+
return url;
|
|
51
|
+
}
|
|
52
|
+
export function isHttpTransient(error) {
|
|
53
|
+
const status = typeof error?.status === "number" ? error.status : null;
|
|
54
|
+
if (status === 429 || (status !== null && status >= 500))
|
|
55
|
+
return true;
|
|
56
|
+
return ["ECONNRESET", "ETIMEDOUT", "ECONNREFUSED", "EPIPE"].includes(String(error?.code ?? ""));
|
|
57
|
+
}
|
|
58
|
+
export function postJson(url, body, headers, timeoutMs, extractErrorMessage = defaultErrorMessage, signal) {
|
|
59
|
+
const payload = JSON.stringify(body);
|
|
60
|
+
const requester = url.protocol === "https:" ? httpsRequest : httpRequest;
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
if (signal?.aborted) {
|
|
63
|
+
reject(new ApiHttpError("API request aborted", null, "", "ABORT_ERR"));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const req = requester(url, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
timeout: timeoutMs,
|
|
69
|
+
headers: {
|
|
70
|
+
"content-type": "application/json",
|
|
71
|
+
accept: "application/json",
|
|
72
|
+
...headers,
|
|
73
|
+
"content-length": Buffer.byteLength(payload),
|
|
74
|
+
},
|
|
75
|
+
}, res => {
|
|
76
|
+
const chunks = [];
|
|
77
|
+
let bytes = 0;
|
|
78
|
+
res.on("data", chunk => {
|
|
79
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
80
|
+
bytes += buf.length;
|
|
81
|
+
if (bytes > MAX_API_RESPONSE_BYTES) {
|
|
82
|
+
req.destroy(new ApiHttpError("API response exceeded the 50MB limit", null));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
chunks.push(buf);
|
|
86
|
+
});
|
|
87
|
+
res.on("end", () => {
|
|
88
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
89
|
+
const status = res.statusCode ?? 0;
|
|
90
|
+
if (status < 200 || status >= 300) {
|
|
91
|
+
reject(new ApiHttpError(extractErrorMessage(status, text), status, text));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
resolve({ status, text });
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
req.on("timeout", () => {
|
|
98
|
+
req.destroy(new ApiHttpError("API request timed out", null, "", "ETIMEDOUT"));
|
|
99
|
+
});
|
|
100
|
+
const onAbort = () => {
|
|
101
|
+
req.destroy(new ApiHttpError("API request aborted", null, "", "ABORT_ERR"));
|
|
102
|
+
};
|
|
103
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
104
|
+
req.on("close", () => signal?.removeEventListener("abort", onAbort));
|
|
105
|
+
req.on("error", reject);
|
|
106
|
+
req.end(payload);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function defaultErrorMessage(status, body) {
|
|
110
|
+
if (!body)
|
|
111
|
+
return `API request failed with HTTP ${status}`;
|
|
112
|
+
try {
|
|
113
|
+
const parsed = JSON.parse(body);
|
|
114
|
+
const message = parsed?.error?.message ?? parsed?.message ?? parsed?.error;
|
|
115
|
+
if (typeof message === "string" && message.length > 0) {
|
|
116
|
+
return `API request failed with HTTP ${status}: ${message}`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
}
|
|
121
|
+
return `API request failed with HTTP ${status}: ${body.slice(0, 1000)}`;
|
|
122
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { URL } from "node:url";
|
|
2
|
+
import type { Logger } from "./logger.js";
|
|
3
|
+
import { ApiHttpError } from "./api-http.js";
|
|
4
|
+
export type ApiProviderKind = "openai-compatible" | "anthropic" | "xai-responses";
|
|
5
|
+
export interface ApiChatMessage {
|
|
6
|
+
role: "system" | "user" | "assistant";
|
|
7
|
+
content: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ApiRequest {
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
apiKey: string;
|
|
12
|
+
model: string;
|
|
13
|
+
messages: ApiChatMessage[];
|
|
14
|
+
maxOutputTokens?: number;
|
|
15
|
+
temperature?: number;
|
|
16
|
+
topP?: number;
|
|
17
|
+
reasoningEffort?: "none" | "low" | "medium" | "high";
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
previousResponseId?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ApiUsage {
|
|
22
|
+
inputTokens?: number;
|
|
23
|
+
outputTokens?: number;
|
|
24
|
+
cacheReadTokens?: number;
|
|
25
|
+
costUsd?: number;
|
|
26
|
+
raw?: unknown;
|
|
27
|
+
}
|
|
28
|
+
export interface ApiResult {
|
|
29
|
+
model: string;
|
|
30
|
+
text: string;
|
|
31
|
+
usage: ApiUsage;
|
|
32
|
+
raw: unknown;
|
|
33
|
+
httpStatus: number;
|
|
34
|
+
responseId?: string | null;
|
|
35
|
+
}
|
|
36
|
+
export interface ApiProvider {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly kind: ApiProviderKind;
|
|
39
|
+
endpointUrl(baseUrl: string): URL;
|
|
40
|
+
buildBody(req: ApiRequest): Record<string, unknown>;
|
|
41
|
+
parseResult(httpStatus: number, body: string): ApiResult;
|
|
42
|
+
authHeaders(apiKey: string): Record<string, string>;
|
|
43
|
+
isTransient(err: unknown): boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare class OpenAiCompatibleProvider implements ApiProvider {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly kind: "openai-compatible";
|
|
48
|
+
constructor(name: string);
|
|
49
|
+
endpointUrl(baseUrl: string): URL;
|
|
50
|
+
buildBody(req: ApiRequest): Record<string, unknown>;
|
|
51
|
+
parseResult(httpStatus: number, body: string): ApiResult;
|
|
52
|
+
authHeaders(apiKey: string): Record<string, string>;
|
|
53
|
+
isTransient(err: unknown): boolean;
|
|
54
|
+
}
|
|
55
|
+
export declare const DEFAULT_ANTHROPIC_VERSION = "2023-06-01";
|
|
56
|
+
export declare class AnthropicProvider implements ApiProvider {
|
|
57
|
+
readonly name: string;
|
|
58
|
+
private readonly anthropicVersion;
|
|
59
|
+
readonly kind: "anthropic";
|
|
60
|
+
constructor(name: string, anthropicVersion?: string);
|
|
61
|
+
endpointUrl(baseUrl: string): URL;
|
|
62
|
+
buildBody(req: ApiRequest): Record<string, unknown>;
|
|
63
|
+
parseResult(httpStatus: number, body: string): ApiResult;
|
|
64
|
+
authHeaders(apiKey: string): Record<string, string>;
|
|
65
|
+
isTransient(err: unknown): boolean;
|
|
66
|
+
}
|
|
67
|
+
export declare class XaiResponsesProvider implements ApiProvider {
|
|
68
|
+
readonly name: string;
|
|
69
|
+
readonly kind: "xai-responses";
|
|
70
|
+
constructor(name: string);
|
|
71
|
+
endpointUrl(baseUrl: string): URL;
|
|
72
|
+
buildBody(req: ApiRequest): Record<string, unknown>;
|
|
73
|
+
parseResult(httpStatus: number, body: string): ApiResult;
|
|
74
|
+
authHeaders(apiKey: string): Record<string, string>;
|
|
75
|
+
isTransient(err: unknown): boolean;
|
|
76
|
+
}
|
|
77
|
+
export declare function createApiProvider(name: string, kind: ApiProviderKind): ApiProvider;
|
|
78
|
+
export declare function resetApiProviderBreakers(): void;
|
|
79
|
+
export declare function apiProviderBreakerState(name: string): string;
|
|
80
|
+
export declare function runApiRequest(provider: ApiProvider, req: ApiRequest, logger?: Logger, opts?: {
|
|
81
|
+
signal?: AbortSignal;
|
|
82
|
+
}): Promise<ApiResult>;
|
|
83
|
+
export { ApiHttpError };
|