@velum-labs/routekit-gateway 0.9.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/LICENSE +201 -0
- package/README.md +28 -0
- package/dist/acp-agent.d.ts +38 -0
- package/dist/acp-agent.js +142 -0
- package/dist/acp-registry.d.ts +36 -0
- package/dist/acp-registry.js +85 -0
- package/dist/adapters/anthropic.d.ts +131 -0
- package/dist/adapters/anthropic.js +1195 -0
- package/dist/adapters/chat.d.ts +14 -0
- package/dist/adapters/chat.js +34 -0
- package/dist/adapters/cursor.d.ts +34 -0
- package/dist/adapters/cursor.js +305 -0
- package/dist/adapters/dropped.d.ts +10 -0
- package/dist/adapters/dropped.js +24 -0
- package/dist/adapters/openai-chat-wire.d.ts +93 -0
- package/dist/adapters/openai-chat-wire.js +143 -0
- package/dist/adapters/responses-stream.d.ts +7 -0
- package/dist/adapters/responses-stream.js +597 -0
- package/dist/adapters/responses.d.ts +174 -0
- package/dist/adapters/responses.js +778 -0
- package/dist/adapters/server-tool-loop.d.ts +94 -0
- package/dist/adapters/server-tool-loop.js +477 -0
- package/dist/adapters/upstream-error.d.ts +14 -0
- package/dist/adapters/upstream-error.js +25 -0
- package/dist/adapters/validate.d.ts +27 -0
- package/dist/adapters/validate.js +180 -0
- package/dist/adapters/web-search.d.ts +46 -0
- package/dist/adapters/web-search.js +151 -0
- package/dist/auth.d.ts +10 -0
- package/dist/auth.js +28 -0
- package/dist/backend.d.ts +151 -0
- package/dist/backend.js +143 -0
- package/dist/capacity-pool.d.ts +31 -0
- package/dist/capacity-pool.js +99 -0
- package/dist/cost.d.ts +49 -0
- package/dist/cost.js +112 -0
- package/dist/endpoint-health.d.ts +54 -0
- package/dist/endpoint-health.js +123 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +23 -0
- package/dist/provenance.d.ts +31 -0
- package/dist/provenance.js +191 -0
- package/dist/provider-backends.d.ts +40 -0
- package/dist/provider-backends.js +1050 -0
- package/dist/provider-source.d.ts +40 -0
- package/dist/provider-source.js +293 -0
- package/dist/router.d.ts +168 -0
- package/dist/router.js +474 -0
- package/dist/server.d.ts +67 -0
- package/dist/server.js +930 -0
- package/dist/sse/chat-assembler.d.ts +45 -0
- package/dist/sse/chat-assembler.js +190 -0
- package/dist/sse/parse.d.ts +50 -0
- package/dist/sse/parse.js +149 -0
- package/dist/sse-wire.d.ts +10 -0
- package/dist/sse-wire.js +31 -0
- package/dist/switching-proxy.d.ts +15 -0
- package/dist/switching-proxy.js +232 -0
- package/dist/test/acp-agent.test.d.ts +1 -0
- package/dist/test/acp-agent.test.js +66 -0
- package/dist/test/acp-registry.test.d.ts +1 -0
- package/dist/test/acp-registry.test.js +70 -0
- package/dist/test/anthropic.test.d.ts +1 -0
- package/dist/test/anthropic.test.js +793 -0
- package/dist/test/auth.test.d.ts +1 -0
- package/dist/test/auth.test.js +25 -0
- package/dist/test/boundary.test.d.ts +1 -0
- package/dist/test/boundary.test.js +32 -0
- package/dist/test/chat.test.d.ts +1 -0
- package/dist/test/chat.test.js +418 -0
- package/dist/test/cost.test.d.ts +1 -0
- package/dist/test/cost.test.js +60 -0
- package/dist/test/cursor.test.d.ts +1 -0
- package/dist/test/cursor.test.js +100 -0
- package/dist/test/drain.test.d.ts +1 -0
- package/dist/test/drain.test.js +116 -0
- package/dist/test/dropped.test.d.ts +1 -0
- package/dist/test/dropped.test.js +80 -0
- package/dist/test/endpoint-health.test.d.ts +1 -0
- package/dist/test/endpoint-health.test.js +73 -0
- package/dist/test/provenance.test.d.ts +1 -0
- package/dist/test/provenance.test.js +176 -0
- package/dist/test/provider-backends.test.d.ts +1 -0
- package/dist/test/provider-backends.test.js +699 -0
- package/dist/test/responses.test.d.ts +1 -0
- package/dist/test/responses.test.js +813 -0
- package/dist/test/routed-backend.test.d.ts +1 -0
- package/dist/test/routed-backend.test.js +39 -0
- package/dist/test/router.test.d.ts +1 -0
- package/dist/test/router.test.js +297 -0
- package/dist/test/server-resilience.test.d.ts +1 -0
- package/dist/test/server-resilience.test.js +169 -0
- package/dist/test/sse-codec.test.d.ts +1 -0
- package/dist/test/sse-codec.test.js +186 -0
- package/dist/test/web-search-loop.test.d.ts +1 -0
- package/dist/test/web-search-loop.test.js +469 -0
- package/dist/test/wire-validation.test.d.ts +1 -0
- package/dist/test/wire-validation.test.js +140 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Product-neutral RouteKit gateway and router. */
|
|
2
|
+
export { startGateway } from "./server.js";
|
|
3
|
+
export { startSwitchingGatewayProxy } from "./switching-proxy.js";
|
|
4
|
+
export { joinPath, ModelRoutedBackend, OpenAiBackend } from "./backend.js";
|
|
5
|
+
export { AnthropicBackend, CodexResponsesBackend, GoogleGenAiBackend } from "./provider-backends.js";
|
|
6
|
+
export { CatalogBackend, isSubscriptionProvider, NoModelAvailableError, normalizeRouterConfigAliases, parseRouterConfig, routerConfigSchema, splitNamespacedModel, UnknownModelError } from "./router.js";
|
|
7
|
+
export { API_PROVIDER_IDS, ApiProviderSource, parseDiscoveredModels, parseReasoningCapabilities, PROVIDER_IDS, SUBSCRIPTION_PROVIDER_IDS } from "./provider-source.js";
|
|
8
|
+
export { endpointHealthProbe, probeEndpointHealth, providerAuthHeaders } from "./endpoint-health.js";
|
|
9
|
+
export { CapacityPool } from "./capacity-pool.js";
|
|
10
|
+
export { effectiveModel, isStream, withDefaultModel } from "./adapters/chat.js";
|
|
11
|
+
export { isCursorChatBody, translateCursorRequest } from "./adapters/cursor.js";
|
|
12
|
+
export { anthropicModelsResponse, anthropicToChat, CLAUDE_ALIAS_PREFIX, chatToAnthropicMessage, claudeModelAlias, countTokensEstimate, handleAnthropicMessages, handleCountTokens, mapStopReason, openAiSseToAnthropic } from "./adapters/anthropic.js";
|
|
13
|
+
export { chatToResponses, customToolNames, handleResponses, openAiSseToResponses, responsesToChat, responsesToolRegistry } from "./adapters/responses.js";
|
|
14
|
+
export { MAX_WEB_SEARCHES_PER_TURN, resolveWebSearchExecutor } from "./adapters/web-search.js";
|
|
15
|
+
export { DIALECT_DROPPED_ATTRIBUTE, droppedField, resetDroppedFieldWarnings, withDroppedFieldSpan } from "./adapters/dropped.js";
|
|
16
|
+
export { ACP_PROTOCOL_VERSION, runAcpAgent } from "./acp-agent.js";
|
|
17
|
+
export { ACP_REGISTRY_URL, fetchAcpRegistry, installAcpAdapters } from "./acp-registry.js";
|
|
18
|
+
export { DEFAULT_MODEL_PRICING, estimateCost, formatUsd, lookupPricing, meterCall, parseUsage, parseUsageFromSse } from "./cost.js";
|
|
19
|
+
export { buildModelCallRecord, MODEL_CALL_ID_HEADER, modelCallId, readProducerVersion, resolveProducerGitSha, responseBodyHash, UNKNOWN_GIT_SHA } from "./provenance.js";
|
|
20
|
+
export { authorizedRequest } from "./auth.js";
|
|
21
|
+
export { errorEvent, finishChunk, noticeChunk, reasoningChunk, sseResponse } from "./sse-wire.js";
|
|
22
|
+
export { ChatStreamAssembler } from "./sse/chat-assembler.js";
|
|
23
|
+
export { decodeBufferedSse, SseDecoder, SseParseError } from "./sse/parse.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ModelCallContract, RequestAttribution } from "@velum-labs/routekit-contracts";
|
|
2
|
+
export type GatewayDialect = "openai-chat" | "openai-embeddings" | "anthropic-messages" | "openai-responses";
|
|
3
|
+
export declare const MODEL_CALL_ID_HEADER = "x-routekit-model-call-id";
|
|
4
|
+
export declare const UNKNOWN_GIT_SHA = "unknown";
|
|
5
|
+
export declare function resolveProducerGitSha(fromDir?: string): string;
|
|
6
|
+
export declare function readProducerVersion(fromDir?: string, fallback?: string): string;
|
|
7
|
+
export type ModelGatewayCallContext = {
|
|
8
|
+
callId: string;
|
|
9
|
+
dialect: GatewayDialect;
|
|
10
|
+
requestedModel: string | undefined;
|
|
11
|
+
model: string | undefined;
|
|
12
|
+
stream: boolean;
|
|
13
|
+
requestBody: unknown;
|
|
14
|
+
startedAt: string;
|
|
15
|
+
endpointId?: string;
|
|
16
|
+
attribution?: RequestAttribution;
|
|
17
|
+
};
|
|
18
|
+
export type ModelGatewayCallResult = {
|
|
19
|
+
statusCode: number;
|
|
20
|
+
responseBody?: Buffer;
|
|
21
|
+
durationMs: number;
|
|
22
|
+
error?: unknown;
|
|
23
|
+
};
|
|
24
|
+
export type ModelCallRecord = ModelCallContract;
|
|
25
|
+
export type ProvenanceSink = {
|
|
26
|
+
onModelCall?(record: ModelCallRecord): void;
|
|
27
|
+
onModelCallRaw?(context: ModelGatewayCallContext, result: ModelGatewayCallResult): void;
|
|
28
|
+
};
|
|
29
|
+
export declare function buildModelCallRecord(context: ModelGatewayCallContext, result: ModelGatewayCallResult): ModelCallRecord;
|
|
30
|
+
export declare function modelCallId(): string;
|
|
31
|
+
export declare function responseBodyHash(body: Buffer): string;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { artifactHash, requestHash, responseHash } from "@velum-labs/routekit-contracts";
|
|
7
|
+
import { meterCall, parseUsage, parseUsageFromSse } from "./cost.js";
|
|
8
|
+
export const MODEL_CALL_ID_HEADER = "x-routekit-model-call-id";
|
|
9
|
+
export const UNKNOWN_GIT_SHA = "unknown";
|
|
10
|
+
const GIT_SHA_PATTERN = /^[a-f0-9]{40}$/;
|
|
11
|
+
function moduleDir() {
|
|
12
|
+
return dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
}
|
|
14
|
+
export function resolveProducerGitSha(fromDir = moduleDir()) {
|
|
15
|
+
const stamped = process.env.ROUTEKIT_BUILD_GIT_SHA?.trim();
|
|
16
|
+
if (stamped !== undefined && GIT_SHA_PATTERN.test(stamped))
|
|
17
|
+
return stamped;
|
|
18
|
+
if (!fromDir.includes("node_modules")) {
|
|
19
|
+
const result = spawnSync("git", ["rev-parse", "HEAD"], { cwd: fromDir, encoding: "utf8" });
|
|
20
|
+
if (result.status === 0 && GIT_SHA_PATTERN.test(result.stdout.trim())) {
|
|
21
|
+
return result.stdout.trim();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return UNKNOWN_GIT_SHA;
|
|
25
|
+
}
|
|
26
|
+
export function readProducerVersion(fromDir = moduleDir(), fallback = "0.0.0") {
|
|
27
|
+
let dir = fromDir;
|
|
28
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
31
|
+
if (typeof parsed.version === "string" && parsed.version.length > 0)
|
|
32
|
+
return parsed.version;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Continue toward the filesystem root.
|
|
36
|
+
}
|
|
37
|
+
const parent = dirname(dir);
|
|
38
|
+
if (parent === dir)
|
|
39
|
+
break;
|
|
40
|
+
dir = parent;
|
|
41
|
+
}
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
function asRecord(value) {
|
|
45
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
46
|
+
? value
|
|
47
|
+
: undefined;
|
|
48
|
+
}
|
|
49
|
+
function parseJson(buffer) {
|
|
50
|
+
if (buffer === undefined || buffer.length === 0)
|
|
51
|
+
return undefined;
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(buffer.toString("utf8"));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function responseText(buffer) {
|
|
60
|
+
return buffer?.toString("utf8") ?? "";
|
|
61
|
+
}
|
|
62
|
+
function requestMessages(body) {
|
|
63
|
+
const messages = asRecord(body)?.messages;
|
|
64
|
+
if (!Array.isArray(messages))
|
|
65
|
+
return [{ role: "user", content: requestHash(body) }];
|
|
66
|
+
const projected = messages.flatMap((message) => {
|
|
67
|
+
const item = asRecord(message);
|
|
68
|
+
const role = item?.role;
|
|
69
|
+
if (role !== "system" && role !== "user" && role !== "assistant" && role !== "tool")
|
|
70
|
+
return [];
|
|
71
|
+
return [{ role, content: requestHash(item?.content ?? "") }];
|
|
72
|
+
});
|
|
73
|
+
return projected.length > 0 ? projected : [{ role: "user", content: requestHash(body) }];
|
|
74
|
+
}
|
|
75
|
+
function usageFromResponse(body) {
|
|
76
|
+
const parsed = asRecord(parseJson(body));
|
|
77
|
+
const usage = parseUsage(parsed?.usage) ?? parseUsageFromSse(responseText(body));
|
|
78
|
+
if (usage === undefined)
|
|
79
|
+
return undefined;
|
|
80
|
+
return {
|
|
81
|
+
...(usage.promptTokens !== undefined ? { prompt_tokens: usage.promptTokens } : {}),
|
|
82
|
+
...(usage.completionTokens !== undefined ? { completion_tokens: usage.completionTokens } : {}),
|
|
83
|
+
...(usage.totalTokens !== undefined ? { total_tokens: usage.totalTokens } : {})
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function providerRequestId(body) {
|
|
87
|
+
const id = asRecord(parseJson(body))?.id;
|
|
88
|
+
return typeof id === "string" && id.length > 0 ? id : undefined;
|
|
89
|
+
}
|
|
90
|
+
function providerError(result) {
|
|
91
|
+
if (result.error === undefined && result.statusCode >= 200 && result.statusCode < 400) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
const responseError = asRecord(asRecord(parseJson(result.responseBody))?.error);
|
|
95
|
+
const noModelAvailable = result.statusCode === 503 &&
|
|
96
|
+
responseError?.type === "unavailable" &&
|
|
97
|
+
responseError.message === "no model is available; configure a provider";
|
|
98
|
+
const kind = noModelAvailable
|
|
99
|
+
? "capability_missing"
|
|
100
|
+
: result.statusCode === 408
|
|
101
|
+
? "timeout"
|
|
102
|
+
: result.statusCode === 429
|
|
103
|
+
? "rate_limited"
|
|
104
|
+
: result.statusCode === 400 || result.statusCode === 422
|
|
105
|
+
? "validation_error"
|
|
106
|
+
: "provider_error";
|
|
107
|
+
const message = kind === "capability_missing"
|
|
108
|
+
? "no model route is configured"
|
|
109
|
+
: kind === "timeout"
|
|
110
|
+
? "provider request timed out"
|
|
111
|
+
: kind === "rate_limited"
|
|
112
|
+
? "provider rate limited the request"
|
|
113
|
+
: kind === "validation_error"
|
|
114
|
+
? "provider rejected the request"
|
|
115
|
+
: "provider request failed";
|
|
116
|
+
return {
|
|
117
|
+
kind,
|
|
118
|
+
message,
|
|
119
|
+
retryable: !noModelAvailable &&
|
|
120
|
+
(result.statusCode === 408 || result.statusCode === 429 || result.statusCode >= 500)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function buildModelCallRecord(context, result) {
|
|
124
|
+
const usage = usageFromResponse(result.responseBody);
|
|
125
|
+
const callCost = meterCall({
|
|
126
|
+
model: context.model ?? context.requestedModel ?? "unknown",
|
|
127
|
+
usage: usage === undefined
|
|
128
|
+
? undefined
|
|
129
|
+
: {
|
|
130
|
+
promptTokens: usage.prompt_tokens,
|
|
131
|
+
completionTokens: usage.completion_tokens,
|
|
132
|
+
totalTokens: usage.total_tokens
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
const error = providerError(result);
|
|
136
|
+
const metadata = {
|
|
137
|
+
dialect: context.dialect,
|
|
138
|
+
stream: context.stream,
|
|
139
|
+
http_status: result.statusCode,
|
|
140
|
+
duration_ms: result.durationMs,
|
|
141
|
+
requested_model: context.requestedModel ?? null,
|
|
142
|
+
unknown_usage: callCost.unknownUsage,
|
|
143
|
+
unknown_cost: callCost.unknownCost,
|
|
144
|
+
...(context.attribution !== undefined
|
|
145
|
+
? {
|
|
146
|
+
attribution: {
|
|
147
|
+
effective_model: context.attribution.effective_model,
|
|
148
|
+
...(context.attribution.native_model !== undefined
|
|
149
|
+
? { native_model: context.attribution.native_model }
|
|
150
|
+
: {}),
|
|
151
|
+
provider: context.attribution.provider,
|
|
152
|
+
billing_mode: context.attribution.billing_mode,
|
|
153
|
+
...(context.attribution.account !== undefined
|
|
154
|
+
? { account: { seat: context.attribution.account.seat } }
|
|
155
|
+
: {}),
|
|
156
|
+
attempts: context.attribution.attempts,
|
|
157
|
+
retries: context.attribution.retries,
|
|
158
|
+
account_failovers: context.attribution.account_failovers
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
: {}),
|
|
162
|
+
...(callCost.costUsd !== undefined ? { cost_estimate_usd: callCost.costUsd } : {})
|
|
163
|
+
};
|
|
164
|
+
return {
|
|
165
|
+
call_id: context.callId,
|
|
166
|
+
endpoint_id: context.endpointId ?? context.dialect,
|
|
167
|
+
...(providerRequestId(result.responseBody) !== undefined
|
|
168
|
+
? { provider_request_id: providerRequestId(result.responseBody) }
|
|
169
|
+
: {}),
|
|
170
|
+
model: context.model ?? context.requestedModel ?? "unknown",
|
|
171
|
+
request_hash: requestHash(context.requestBody),
|
|
172
|
+
...(result.responseBody !== undefined
|
|
173
|
+
? { response_hash: responseHash(responseText(result.responseBody)) }
|
|
174
|
+
: {}),
|
|
175
|
+
messages: requestMessages(context.requestBody),
|
|
176
|
+
status: error === undefined ? "succeeded" : "failed",
|
|
177
|
+
side_effects: "none",
|
|
178
|
+
started_at: context.startedAt,
|
|
179
|
+
finished_at: new Date(new Date(context.startedAt).getTime() + result.durationMs).toISOString(),
|
|
180
|
+
latency_ms: result.durationMs,
|
|
181
|
+
...(usage !== undefined ? { usage } : {}),
|
|
182
|
+
...(error !== undefined ? { error } : {}),
|
|
183
|
+
metadata
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export function modelCallId() {
|
|
187
|
+
return `model_call_${randomUUID()}`;
|
|
188
|
+
}
|
|
189
|
+
export function responseBodyHash(body) {
|
|
190
|
+
return artifactHash(body);
|
|
191
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Backend, BackendRequestOptions } from "./backend.js";
|
|
2
|
+
export type ProviderBackendOptions = {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
defaultModel?: string;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
transport?: ProviderTransport;
|
|
8
|
+
forceStream?: boolean;
|
|
9
|
+
omitSampling?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type ProviderTransport = (url: string, init: RequestInit, options?: BackendRequestOptions) => Promise<Response>;
|
|
12
|
+
declare abstract class HttpProviderBackend implements Backend {
|
|
13
|
+
readonly defaultModel: string | undefined;
|
|
14
|
+
readonly baseUrl: string;
|
|
15
|
+
readonly apiKey: string;
|
|
16
|
+
readonly extraHeaders: Record<string, string>;
|
|
17
|
+
readonly transport: ProviderTransport;
|
|
18
|
+
constructor(options: ProviderBackendOptions);
|
|
19
|
+
listModelIds(): readonly string[];
|
|
20
|
+
servesModel(model: string): boolean;
|
|
21
|
+
models(): Promise<Response>;
|
|
22
|
+
embeddings(): Promise<Response>;
|
|
23
|
+
abstract chat(body: unknown, signal?: AbortSignal, options?: BackendRequestOptions): Promise<Response>;
|
|
24
|
+
}
|
|
25
|
+
export declare class AnthropicBackend extends HttpProviderBackend {
|
|
26
|
+
#private;
|
|
27
|
+
chat(body: unknown, signal?: AbortSignal, options?: BackendRequestOptions): Promise<Response>;
|
|
28
|
+
}
|
|
29
|
+
export declare class GoogleGenAiBackend extends HttpProviderBackend {
|
|
30
|
+
#private;
|
|
31
|
+
chat(body: unknown, signal?: AbortSignal, options?: BackendRequestOptions): Promise<Response>;
|
|
32
|
+
}
|
|
33
|
+
export declare class CodexResponsesBackend extends HttpProviderBackend {
|
|
34
|
+
#private;
|
|
35
|
+
constructor(options: ProviderBackendOptions & {
|
|
36
|
+
accountId?: string;
|
|
37
|
+
});
|
|
38
|
+
chat(body: unknown, signal?: AbortSignal, options?: BackendRequestOptions): Promise<Response>;
|
|
39
|
+
}
|
|
40
|
+
export {};
|