@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.
Files changed (99) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +28 -0
  3. package/dist/acp-agent.d.ts +38 -0
  4. package/dist/acp-agent.js +142 -0
  5. package/dist/acp-registry.d.ts +36 -0
  6. package/dist/acp-registry.js +85 -0
  7. package/dist/adapters/anthropic.d.ts +131 -0
  8. package/dist/adapters/anthropic.js +1195 -0
  9. package/dist/adapters/chat.d.ts +14 -0
  10. package/dist/adapters/chat.js +34 -0
  11. package/dist/adapters/cursor.d.ts +34 -0
  12. package/dist/adapters/cursor.js +305 -0
  13. package/dist/adapters/dropped.d.ts +10 -0
  14. package/dist/adapters/dropped.js +24 -0
  15. package/dist/adapters/openai-chat-wire.d.ts +93 -0
  16. package/dist/adapters/openai-chat-wire.js +143 -0
  17. package/dist/adapters/responses-stream.d.ts +7 -0
  18. package/dist/adapters/responses-stream.js +597 -0
  19. package/dist/adapters/responses.d.ts +174 -0
  20. package/dist/adapters/responses.js +778 -0
  21. package/dist/adapters/server-tool-loop.d.ts +94 -0
  22. package/dist/adapters/server-tool-loop.js +477 -0
  23. package/dist/adapters/upstream-error.d.ts +14 -0
  24. package/dist/adapters/upstream-error.js +25 -0
  25. package/dist/adapters/validate.d.ts +27 -0
  26. package/dist/adapters/validate.js +180 -0
  27. package/dist/adapters/web-search.d.ts +46 -0
  28. package/dist/adapters/web-search.js +151 -0
  29. package/dist/auth.d.ts +10 -0
  30. package/dist/auth.js +28 -0
  31. package/dist/backend.d.ts +151 -0
  32. package/dist/backend.js +143 -0
  33. package/dist/capacity-pool.d.ts +31 -0
  34. package/dist/capacity-pool.js +99 -0
  35. package/dist/cost.d.ts +49 -0
  36. package/dist/cost.js +112 -0
  37. package/dist/endpoint-health.d.ts +54 -0
  38. package/dist/endpoint-health.js +123 -0
  39. package/dist/index.d.ts +40 -0
  40. package/dist/index.js +23 -0
  41. package/dist/provenance.d.ts +31 -0
  42. package/dist/provenance.js +191 -0
  43. package/dist/provider-backends.d.ts +40 -0
  44. package/dist/provider-backends.js +1050 -0
  45. package/dist/provider-source.d.ts +40 -0
  46. package/dist/provider-source.js +293 -0
  47. package/dist/router.d.ts +168 -0
  48. package/dist/router.js +474 -0
  49. package/dist/server.d.ts +67 -0
  50. package/dist/server.js +930 -0
  51. package/dist/sse/chat-assembler.d.ts +45 -0
  52. package/dist/sse/chat-assembler.js +190 -0
  53. package/dist/sse/parse.d.ts +50 -0
  54. package/dist/sse/parse.js +149 -0
  55. package/dist/sse-wire.d.ts +10 -0
  56. package/dist/sse-wire.js +31 -0
  57. package/dist/switching-proxy.d.ts +15 -0
  58. package/dist/switching-proxy.js +232 -0
  59. package/dist/test/acp-agent.test.d.ts +1 -0
  60. package/dist/test/acp-agent.test.js +66 -0
  61. package/dist/test/acp-registry.test.d.ts +1 -0
  62. package/dist/test/acp-registry.test.js +70 -0
  63. package/dist/test/anthropic.test.d.ts +1 -0
  64. package/dist/test/anthropic.test.js +793 -0
  65. package/dist/test/auth.test.d.ts +1 -0
  66. package/dist/test/auth.test.js +25 -0
  67. package/dist/test/boundary.test.d.ts +1 -0
  68. package/dist/test/boundary.test.js +32 -0
  69. package/dist/test/chat.test.d.ts +1 -0
  70. package/dist/test/chat.test.js +418 -0
  71. package/dist/test/cost.test.d.ts +1 -0
  72. package/dist/test/cost.test.js +60 -0
  73. package/dist/test/cursor.test.d.ts +1 -0
  74. package/dist/test/cursor.test.js +100 -0
  75. package/dist/test/drain.test.d.ts +1 -0
  76. package/dist/test/drain.test.js +116 -0
  77. package/dist/test/dropped.test.d.ts +1 -0
  78. package/dist/test/dropped.test.js +80 -0
  79. package/dist/test/endpoint-health.test.d.ts +1 -0
  80. package/dist/test/endpoint-health.test.js +73 -0
  81. package/dist/test/provenance.test.d.ts +1 -0
  82. package/dist/test/provenance.test.js +176 -0
  83. package/dist/test/provider-backends.test.d.ts +1 -0
  84. package/dist/test/provider-backends.test.js +699 -0
  85. package/dist/test/responses.test.d.ts +1 -0
  86. package/dist/test/responses.test.js +813 -0
  87. package/dist/test/routed-backend.test.d.ts +1 -0
  88. package/dist/test/routed-backend.test.js +39 -0
  89. package/dist/test/router.test.d.ts +1 -0
  90. package/dist/test/router.test.js +297 -0
  91. package/dist/test/server-resilience.test.d.ts +1 -0
  92. package/dist/test/server-resilience.test.js +169 -0
  93. package/dist/test/sse-codec.test.d.ts +1 -0
  94. package/dist/test/sse-codec.test.js +186 -0
  95. package/dist/test/web-search-loop.test.d.ts +1 -0
  96. package/dist/test/web-search-loop.test.js +469 -0
  97. package/dist/test/wire-validation.test.d.ts +1 -0
  98. package/dist/test/wire-validation.test.js +140 -0
  99. 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 {};