@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
@@ -0,0 +1,143 @@
1
+ import { reasoningSelectionOf } from "./adapters/openai-chat-wire.js";
2
+ /** Join a base URL (which may end in `/`) with a route path. */
3
+ export function joinPath(baseUrl, path) {
4
+ const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
5
+ const suffix = path.startsWith("/") ? path : `/${path}`;
6
+ return `${base}${suffix}`;
7
+ }
8
+ /** An OpenAI Chat Completions backend reached over HTTP. */
9
+ export class OpenAiBackend {
10
+ #baseUrl;
11
+ #apiKey;
12
+ #forceModel;
13
+ #extraHeaders;
14
+ defaultModel;
15
+ constructor(options) {
16
+ this.#baseUrl = options.baseUrl;
17
+ this.#apiKey = options.apiKey ?? "not-needed";
18
+ this.#forceModel = options.forceModel;
19
+ this.#extraHeaders = options.headers ?? {};
20
+ this.defaultModel = options.defaultModel;
21
+ }
22
+ #headers(options = {}) {
23
+ return {
24
+ "content-type": "application/json",
25
+ authorization: `Bearer ${this.#apiKey}`,
26
+ ...this.#extraHeaders,
27
+ ...(options.modelCallId ? { "x-routekit-model-call-id": options.modelCallId } : {})
28
+ };
29
+ }
30
+ chat(body, signal, options = {}) {
31
+ const routed = this.#forceModel !== undefined && typeof body === "object" && body !== null && !Array.isArray(body)
32
+ ? { ...body, model: this.#forceModel }
33
+ : body;
34
+ const selection = reasoningSelectionOf(routed);
35
+ if ((selection.mode === "budget" || selection.mode === "adaptive") &&
36
+ options.reasoningCapabilities?.wireShape !== "openrouter") {
37
+ return Promise.resolve(Response.json({
38
+ error: {
39
+ type: "invalid_request_error",
40
+ message: `OpenAI Chat cannot represent reasoning mode "${selection.mode}"`
41
+ }
42
+ }, { status: 400 }));
43
+ }
44
+ const payload = options.reasoningCapabilities?.wireShape === "openrouter" &&
45
+ routed !== null &&
46
+ typeof routed === "object" &&
47
+ !Array.isArray(routed)
48
+ ? this.#openRouterReasoning(routed, selection)
49
+ : routed;
50
+ return fetch(joinPath(this.#baseUrl, "/chat/completions"), {
51
+ method: "POST",
52
+ headers: this.#headers(options),
53
+ body: JSON.stringify(payload),
54
+ ...(signal ? { signal } : {})
55
+ });
56
+ }
57
+ #openRouterReasoning(body, selection) {
58
+ const payload = { ...body };
59
+ delete payload.reasoning_effort;
60
+ if (selection.mode === "effort") {
61
+ payload.reasoning = { effort: selection.effort };
62
+ }
63
+ else if (selection.mode === "budget") {
64
+ payload.reasoning = { max_tokens: selection.budgetTokens };
65
+ }
66
+ else if (selection.mode === "adaptive") {
67
+ payload.reasoning = { enabled: true };
68
+ }
69
+ else if (selection.mode === "disabled") {
70
+ payload.reasoning = { enabled: false };
71
+ }
72
+ return payload;
73
+ }
74
+ models(signal) {
75
+ return fetch(joinPath(this.#baseUrl, "/models"), {
76
+ method: "GET",
77
+ headers: this.#headers(),
78
+ ...(signal ? { signal } : {})
79
+ });
80
+ }
81
+ embeddings(body, signal, options = {}) {
82
+ return fetch(joinPath(this.#baseUrl, "/embeddings"), {
83
+ method: "POST",
84
+ headers: this.#headers(options),
85
+ body: JSON.stringify(body),
86
+ ...(signal ? { signal } : {})
87
+ });
88
+ }
89
+ }
90
+ /**
91
+ * A backend that dispatches by requested model id: ids in `routedModelIds` go
92
+ * to the `routed` backend, everything else to `primary`. This lets selected
93
+ * model ids use a secondary destination.
94
+ */
95
+ export class ModelRoutedBackend {
96
+ #routedIds;
97
+ #routed;
98
+ #primary;
99
+ defaultModel;
100
+ constructor(options) {
101
+ this.#routedIds = new Set(options.routedModelIds);
102
+ this.#routed = options.routed;
103
+ this.#primary = options.primary;
104
+ this.defaultModel = options.primary.defaultModel;
105
+ }
106
+ #backendFor(model) {
107
+ return model !== undefined && this.#routedIds.has(model) ? this.#routed : this.#primary;
108
+ }
109
+ listModelIds() {
110
+ const ids = [...(this.#primary.listModelIds?.() ?? (this.defaultModel !== undefined ? [this.defaultModel] : []))];
111
+ for (const id of this.#routedIds) {
112
+ if (!ids.includes(id))
113
+ ids.push(id);
114
+ }
115
+ return ids;
116
+ }
117
+ resolveModel(requested) {
118
+ if (requested !== undefined && this.#routedIds.has(requested))
119
+ return requested;
120
+ return this.#primary.resolveModel?.(requested) ?? this.#primary.defaultModel;
121
+ }
122
+ chat(body, signal, options = {}) {
123
+ const model = typeof body === "object" && body !== null && typeof body.model === "string"
124
+ ? body.model
125
+ : undefined;
126
+ return this.#backendFor(model).chat(body, signal, options);
127
+ }
128
+ models(signal) {
129
+ return this.#primary.models(signal);
130
+ }
131
+ embeddings(body, signal, options) {
132
+ const model = typeof body === "object" &&
133
+ body !== null &&
134
+ typeof body.model === "string"
135
+ ? body.model
136
+ : undefined;
137
+ return this.#backendFor(model).embeddings(body, signal, options);
138
+ }
139
+ async close() {
140
+ await this.#primary.close?.();
141
+ await this.#routed.close?.();
142
+ }
143
+ }
@@ -0,0 +1,31 @@
1
+ export type CapacityPoolStrategy = "sticky" | "round_robin" | "capacity_weighted";
2
+ export type CapacityPoolMember<T> = {
3
+ id: string;
4
+ value: T;
5
+ capacity?: number;
6
+ healthy?: boolean;
7
+ coolingUntil?: number;
8
+ quotaUtilization?: number;
9
+ };
10
+ export type CapacityLease<T> = {
11
+ readonly id: string;
12
+ readonly value: T;
13
+ release(): void;
14
+ };
15
+ export type CapacityPoolOptions = {
16
+ strategy?: CapacityPoolStrategy;
17
+ now?: () => number;
18
+ };
19
+ /**
20
+ * Generic health-, quota-, and cooldown-aware selector. Values are opaque, so
21
+ * the same policy can balance HTTP endpoint instances and provider accounts.
22
+ */
23
+ export declare class CapacityPool<T> {
24
+ #private;
25
+ constructor(members: readonly CapacityPoolMember<T>[], options?: CapacityPoolOptions);
26
+ list(): readonly CapacityPoolMember<T>[];
27
+ acquire(stickyKey?: string, excluded?: ReadonlySet<string>): CapacityLease<T>;
28
+ update(id: string, state: Partial<Omit<CapacityPoolMember<T>, "id" | "value">>): void;
29
+ markHealthy(id: string): void;
30
+ markFailure(id: string, cooldownMs: number): void;
31
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Generic health-, quota-, and cooldown-aware selector. Values are opaque, so
3
+ * the same policy can balance HTTP endpoint instances and provider accounts.
4
+ */
5
+ export class CapacityPool {
6
+ #members;
7
+ #strategy;
8
+ #now;
9
+ #sticky = new Map();
10
+ #roundRobin = 0;
11
+ constructor(members, options = {}) {
12
+ if (members.length === 0)
13
+ throw new Error("capacity pool requires at least one member");
14
+ const ids = new Set();
15
+ this.#members = members.map((member) => {
16
+ if (ids.has(member.id))
17
+ throw new Error(`duplicate capacity pool member: ${member.id}`);
18
+ ids.add(member.id);
19
+ return { ...member, inFlight: 0, lastUsed: 0 };
20
+ });
21
+ this.#strategy = options.strategy ?? "capacity_weighted";
22
+ this.#now = options.now ?? Date.now;
23
+ }
24
+ list() {
25
+ return this.#members.map(({ inFlight: _inFlight, lastUsed: _lastUsed, ...member }) => member);
26
+ }
27
+ acquire(stickyKey = "default", excluded = new Set()) {
28
+ const now = this.#now();
29
+ const eligible = this.#members.filter((member) => !excluded.has(member.id) &&
30
+ member.healthy !== false &&
31
+ (member.coolingUntil === undefined || member.coolingUntil <= now) &&
32
+ (member.quotaUtilization === undefined || member.quotaUtilization < 1));
33
+ if (eligible.length === 0)
34
+ throw new Error("capacity pool is exhausted");
35
+ const selected = this.#select(eligible, stickyKey);
36
+ selected.inFlight += 1;
37
+ selected.lastUsed = now;
38
+ this.#sticky.set(stickyKey, selected.id);
39
+ let released = false;
40
+ return {
41
+ id: selected.id,
42
+ value: selected.value,
43
+ release: () => {
44
+ if (released)
45
+ return;
46
+ released = true;
47
+ selected.inFlight = Math.max(0, selected.inFlight - 1);
48
+ }
49
+ };
50
+ }
51
+ update(id, state) {
52
+ const member = this.#member(id);
53
+ Object.assign(member, state);
54
+ if (state.healthy === false) {
55
+ for (const [key, stickyId] of this.#sticky) {
56
+ if (stickyId === id)
57
+ this.#sticky.delete(key);
58
+ }
59
+ }
60
+ }
61
+ markHealthy(id) {
62
+ this.update(id, { healthy: true, coolingUntil: undefined });
63
+ }
64
+ markFailure(id, cooldownMs) {
65
+ this.update(id, {
66
+ coolingUntil: this.#now() + Math.max(0, cooldownMs)
67
+ });
68
+ }
69
+ #member(id) {
70
+ const member = this.#members.find((candidate) => candidate.id === id);
71
+ if (member === undefined)
72
+ throw new Error(`unknown capacity pool member: ${id}`);
73
+ return member;
74
+ }
75
+ #select(eligible, stickyKey) {
76
+ switch (this.#strategy) {
77
+ case "sticky": {
78
+ const stickyId = this.#sticky.get(stickyKey);
79
+ return eligible.find((member) => member.id === stickyId) ?? eligible[0];
80
+ }
81
+ case "round_robin": {
82
+ const selected = eligible[this.#roundRobin % eligible.length];
83
+ this.#roundRobin += 1;
84
+ return selected;
85
+ }
86
+ case "capacity_weighted":
87
+ return [...eligible].sort((left, right) => this.#score(right) - this.#score(left))[0];
88
+ default: {
89
+ const unreachable = this.#strategy;
90
+ throw new Error(`unhandled capacity pool strategy: ${String(unreachable)}`);
91
+ }
92
+ }
93
+ }
94
+ #score(member) {
95
+ const capacity = Math.max(1, member.capacity ?? 1);
96
+ const quotaHeadroom = 1 - (member.quotaUtilization ?? 0);
97
+ return (capacity * quotaHeadroom) / (member.inFlight + 1);
98
+ }
99
+ }
package/dist/cost.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ export type ModelPricing = {
2
+ inputPer1mTokens: number;
3
+ outputPer1mTokens: number;
4
+ currency?: string;
5
+ };
6
+ export type TokenUsage = {
7
+ promptTokens?: number;
8
+ completionTokens?: number;
9
+ totalTokens?: number;
10
+ };
11
+ export type ProviderCostMetadata = {
12
+ source: "provider" | "estimate";
13
+ costUsd?: number;
14
+ generationId?: string;
15
+ providerName?: string;
16
+ upstreamInferenceCost?: number;
17
+ cacheDiscount?: number;
18
+ lookupStatus?: string;
19
+ tokensPrompt?: number;
20
+ tokensCompletion?: number;
21
+ nativeTokensPrompt?: number;
22
+ nativeTokensCompletion?: number;
23
+ };
24
+ export type CallCostRecord = {
25
+ model: string;
26
+ usage: TokenUsage;
27
+ costUsd?: number;
28
+ providerCostUsd?: number;
29
+ unknownUsage: boolean;
30
+ unknownCost: boolean;
31
+ partialUsage?: boolean;
32
+ currency: string;
33
+ providerCost?: ProviderCostMetadata;
34
+ };
35
+ export declare const DEFAULT_MODEL_PRICING: Readonly<Record<string, ModelPricing>>;
36
+ export declare function parseUsage(value: unknown): TokenUsage | undefined;
37
+ export declare function parseUsageFromSse(text: string): TokenUsage | undefined;
38
+ export declare function lookupPricing(model: string, overrides?: Readonly<Record<string, ModelPricing>>): ModelPricing | undefined;
39
+ export declare function estimateCost(usage: TokenUsage, pricing: ModelPricing | undefined): {
40
+ costUsd: number;
41
+ partialUsage: boolean;
42
+ } | undefined;
43
+ export declare function meterCall(input: {
44
+ model: string;
45
+ usage?: TokenUsage;
46
+ pricing?: Readonly<Record<string, ModelPricing>>;
47
+ providerCost?: ProviderCostMetadata;
48
+ }): CallCostRecord;
49
+ export declare function formatUsd(amount: number, currency?: string): string;
package/dist/cost.js ADDED
@@ -0,0 +1,112 @@
1
+ import { DEFAULT_MODEL_PRICING as REGISTRY_MODEL_PRICING, PRICING_ALIASES } from "@velum-labs/routekit-registry";
2
+ import { decodeBufferedSse } from "./sse/parse.js";
3
+ const DEFAULT_CURRENCY = "USD";
4
+ export const DEFAULT_MODEL_PRICING = REGISTRY_MODEL_PRICING;
5
+ function asRecord(value) {
6
+ return typeof value === "object" && value !== null && !Array.isArray(value)
7
+ ? value
8
+ : undefined;
9
+ }
10
+ function nonnegativeNumber(value) {
11
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
12
+ }
13
+ export function parseUsage(value) {
14
+ const usage = asRecord(value);
15
+ if (usage === undefined)
16
+ return undefined;
17
+ const prompt = nonnegativeNumber(usage.prompt_tokens) ?? nonnegativeNumber(usage.input_tokens);
18
+ const completion = nonnegativeNumber(usage.completion_tokens) ?? nonnegativeNumber(usage.output_tokens);
19
+ const total = nonnegativeNumber(usage.total_tokens) ??
20
+ (prompt !== undefined && completion !== undefined ? prompt + completion : undefined);
21
+ if (prompt === undefined && completion === undefined && total === undefined)
22
+ return undefined;
23
+ return {
24
+ ...(prompt !== undefined ? { promptTokens: prompt } : {}),
25
+ ...(completion !== undefined ? { completionTokens: completion } : {}),
26
+ ...(total !== undefined ? { totalTokens: total } : {})
27
+ };
28
+ }
29
+ export function parseUsageFromSse(text) {
30
+ let usage;
31
+ for (const event of decodeBufferedSse(text)) {
32
+ const data = event.data.trim();
33
+ if (data.length === 0 || data === "[DONE]")
34
+ continue;
35
+ try {
36
+ const parsed = asRecord(JSON.parse(data));
37
+ const candidate = parseUsage(parsed?.usage) ??
38
+ parseUsage(asRecord(parsed?.response)?.usage);
39
+ if (candidate !== undefined)
40
+ usage = candidate;
41
+ }
42
+ catch {
43
+ // Usage extraction is observational and ignores malformed events.
44
+ }
45
+ }
46
+ return usage;
47
+ }
48
+ function canonicalPricingKey(model) {
49
+ const direct = Object.keys(DEFAULT_MODEL_PRICING).find((candidate) => candidate.toLowerCase() === model.toLowerCase());
50
+ if (direct !== undefined)
51
+ return direct;
52
+ const alias = Object.entries(PRICING_ALIASES).find(([candidate]) => candidate.toLowerCase() === model.toLowerCase());
53
+ return alias?.[1] ?? model;
54
+ }
55
+ export function lookupPricing(model, overrides = {}) {
56
+ const combined = {
57
+ ...DEFAULT_MODEL_PRICING,
58
+ ...overrides
59
+ };
60
+ const key = canonicalPricingKey(model);
61
+ const entry = Object.entries(combined).find(([candidate]) => candidate.toLowerCase() === key.toLowerCase());
62
+ return entry?.[1];
63
+ }
64
+ export function estimateCost(usage, pricing) {
65
+ if (pricing === undefined)
66
+ return undefined;
67
+ const hasPrompt = usage.promptTokens !== undefined;
68
+ const hasCompletion = usage.completionTokens !== undefined;
69
+ if (!hasPrompt && !hasCompletion)
70
+ return undefined;
71
+ return {
72
+ costUsd: ((usage.promptTokens ?? 0) * pricing.inputPer1mTokens +
73
+ (usage.completionTokens ?? 0) * pricing.outputPer1mTokens) /
74
+ 1_000_000,
75
+ partialUsage: !hasPrompt || !hasCompletion
76
+ };
77
+ }
78
+ export function meterCall(input) {
79
+ const usage = input.usage ?? {};
80
+ const providerLookupFailed = input.providerCost?.source === "provider" &&
81
+ input.providerCost.costUsd === undefined &&
82
+ input.providerCost.lookupStatus !== undefined;
83
+ const configuredPricing = input.pricing ?? {};
84
+ const pricing = providerLookupFailed
85
+ ? Object.entries(configuredPricing).find(([candidate]) => candidate.toLowerCase() === input.model.toLowerCase())?.[1]
86
+ : lookupPricing(input.model, configuredPricing);
87
+ const estimate = estimateCost(usage, pricing);
88
+ const exact = input.providerCost?.source === "provider" ? input.providerCost.costUsd : undefined;
89
+ const providerCostUsd = exact ?? estimate?.costUsd;
90
+ const providerCost = providerLookupFailed && providerCostUsd !== undefined
91
+ ? {
92
+ ...input.providerCost,
93
+ source: "estimate",
94
+ costUsd: providerCostUsd,
95
+ lookupStatus: `fallback_${input.providerCost?.lookupStatus}`
96
+ }
97
+ : input.providerCost;
98
+ return {
99
+ model: input.model,
100
+ usage,
101
+ ...(providerCostUsd !== undefined ? { costUsd: providerCostUsd, providerCostUsd } : {}),
102
+ unknownUsage: input.usage === undefined,
103
+ unknownCost: providerCostUsd === undefined,
104
+ ...(estimate?.partialUsage === true ? { partialUsage: true } : {}),
105
+ currency: pricing?.currency ?? DEFAULT_CURRENCY,
106
+ ...(providerCost !== undefined ? { providerCost } : {})
107
+ };
108
+ }
109
+ export function formatUsd(amount, currency = DEFAULT_CURRENCY) {
110
+ const digits = amount >= 1 ? 2 : 4;
111
+ return `${currency === "USD" ? "$" : ""}${amount.toFixed(digits)}${currency === "USD" ? "" : ` ${currency}`}`;
112
+ }
@@ -0,0 +1,54 @@
1
+ import type { ProviderAuthStyle } from "@velum-labs/routekit-registry";
2
+ export type UrlEndpointConfig = {
3
+ endpointId: string;
4
+ model: string;
5
+ provider?: string;
6
+ baseUrl: string;
7
+ dialect: "openai" | "anthropic" | "google" | "codex";
8
+ headers?: Record<string, string>;
9
+ };
10
+ export type AccountEndpointConfig = {
11
+ endpointId: string;
12
+ model: string;
13
+ account: "claude-code" | "codex";
14
+ };
15
+ export type ModelEndpointConfig = UrlEndpointConfig | AccountEndpointConfig;
16
+ export type EndpointHealthProbe = {
17
+ url: string;
18
+ headers: Readonly<Record<string, string>>;
19
+ invalidStatuses: readonly number[];
20
+ };
21
+ export type EndpointHealthProbePlan = {
22
+ supported: true;
23
+ probe: EndpointHealthProbe;
24
+ } | {
25
+ supported: false;
26
+ reason: string;
27
+ };
28
+ export type EndpointHealthResult = {
29
+ kind: "response";
30
+ ok: boolean;
31
+ status: number;
32
+ authRejected: boolean;
33
+ } | {
34
+ kind: "unsupported";
35
+ reason: string;
36
+ } | {
37
+ kind: "error";
38
+ error: string;
39
+ };
40
+ type Fetcher = (input: string | URL, init?: RequestInit) => Promise<Response>;
41
+ /** Build the registry-defined authentication header without mutating callers. */
42
+ export declare function providerAuthHeaders(auth: ProviderAuthStyle, credential: string | undefined): Record<string, string>;
43
+ /**
44
+ * Resolve a read-only endpoint health probe from registry metadata, falling
45
+ * back to the configured provider-native dialect for custom endpoints.
46
+ */
47
+ export declare function endpointHealthProbe(endpoint: ModelEndpointConfig, credential?: string): EndpointHealthProbePlan;
48
+ /** Execute a provider-native health probe without returning request secrets. */
49
+ export declare function probeEndpointHealth(endpoint: ModelEndpointConfig, options?: {
50
+ credential?: string;
51
+ timeoutMs?: number;
52
+ fetchImpl?: Fetcher;
53
+ }): Promise<EndpointHealthResult>;
54
+ export {};
@@ -0,0 +1,123 @@
1
+ import { PROVIDERS, providerKeyProbe } from "@velum-labs/routekit-registry";
2
+ function isAccountEndpointConfig(endpoint) {
3
+ return "account" in endpoint;
4
+ }
5
+ function nativeDialectProbe(dialect) {
6
+ switch (dialect) {
7
+ case "openai":
8
+ return { path: "/models", auth: "bearer", invalidStatuses: [401, 403] };
9
+ case "anthropic":
10
+ return {
11
+ path: "/models",
12
+ auth: "x-api-key",
13
+ extraHeaders: { "anthropic-version": "2023-06-01" },
14
+ invalidStatuses: [401, 403]
15
+ };
16
+ case "google":
17
+ return {
18
+ path: "/models",
19
+ auth: "x-goog-api-key",
20
+ invalidStatuses: [400, 401, 403]
21
+ };
22
+ case "codex":
23
+ return undefined;
24
+ default: {
25
+ const exhaustive = dialect;
26
+ throw new Error(`unknown endpoint dialect: ${String(exhaustive)}`);
27
+ }
28
+ }
29
+ }
30
+ /** Build the registry-defined authentication header without mutating callers. */
31
+ export function providerAuthHeaders(auth, credential) {
32
+ if (credential === undefined || credential.length === 0)
33
+ return {};
34
+ switch (auth) {
35
+ case "bearer":
36
+ return { authorization: `Bearer ${credential}` };
37
+ case "x-api-key":
38
+ return { "x-api-key": credential };
39
+ case "x-goog-api-key":
40
+ return { "x-goog-api-key": credential };
41
+ default: {
42
+ const exhaustive = auth;
43
+ throw new Error(`unknown provider authentication style: ${String(exhaustive)}`);
44
+ }
45
+ }
46
+ }
47
+ function probeUrl(baseUrl, probePath) {
48
+ const url = new URL(baseUrl);
49
+ const baseSegments = url.pathname.split("/").filter(Boolean);
50
+ const probeSegments = probePath.split("/").filter(Boolean);
51
+ let overlap = Math.min(baseSegments.length, probeSegments.length);
52
+ while (overlap > 0 &&
53
+ !baseSegments
54
+ .slice(baseSegments.length - overlap)
55
+ .every((segment, index) => segment === probeSegments[index])) {
56
+ overlap -= 1;
57
+ }
58
+ url.pathname = `/${[...baseSegments, ...probeSegments.slice(overlap)].join("/")}`;
59
+ url.search = "";
60
+ url.hash = "";
61
+ return url.toString();
62
+ }
63
+ /**
64
+ * Resolve a read-only endpoint health probe from registry metadata, falling
65
+ * back to the configured provider-native dialect for custom endpoints.
66
+ */
67
+ export function endpointHealthProbe(endpoint, credential) {
68
+ if (isAccountEndpointConfig(endpoint)) {
69
+ return {
70
+ supported: false,
71
+ reason: "account-backed endpoint health is reported by the subscription pool"
72
+ };
73
+ }
74
+ const registryProbe = endpoint.provider !== undefined ? providerKeyProbe(endpoint.provider) : undefined;
75
+ const providerInfo = endpoint.provider !== undefined ? PROVIDERS[endpoint.provider] : undefined;
76
+ if (registryProbe === undefined &&
77
+ providerInfo?.apiCompatibility === "openai-responses") {
78
+ return {
79
+ supported: false,
80
+ reason: "no safe read-only health probe is defined for the Codex responses provider"
81
+ };
82
+ }
83
+ const metadata = registryProbe ?? nativeDialectProbe(endpoint.dialect);
84
+ if (metadata === undefined) {
85
+ return {
86
+ supported: false,
87
+ reason: "no safe read-only health probe is defined for the Codex responses dialect"
88
+ };
89
+ }
90
+ return {
91
+ supported: true,
92
+ probe: {
93
+ url: probeUrl(endpoint.baseUrl, metadata.path),
94
+ headers: {
95
+ ...endpoint.headers,
96
+ ...metadata.extraHeaders,
97
+ ...providerAuthHeaders(metadata.auth, credential)
98
+ },
99
+ invalidStatuses: metadata.invalidStatuses
100
+ }
101
+ };
102
+ }
103
+ /** Execute a provider-native health probe without returning request secrets. */
104
+ export async function probeEndpointHealth(endpoint, options = {}) {
105
+ const plan = endpointHealthProbe(endpoint, options.credential);
106
+ if (!plan.supported)
107
+ return { kind: "unsupported", reason: plan.reason };
108
+ try {
109
+ const response = await (options.fetchImpl ?? fetch)(plan.probe.url, {
110
+ headers: plan.probe.headers,
111
+ signal: AbortSignal.timeout(options.timeoutMs ?? 5_000)
112
+ });
113
+ return {
114
+ kind: "response",
115
+ ok: response.ok,
116
+ status: response.status,
117
+ authRejected: plan.probe.invalidStatuses.includes(response.status)
118
+ };
119
+ }
120
+ catch {
121
+ return { kind: "error", error: "health probe request failed" };
122
+ }
123
+ }
@@ -0,0 +1,40 @@
1
+ /** Product-neutral RouteKit gateway and router. */
2
+ export { startGateway } from "./server.js";
3
+ export type { Gateway, GatewayOptions, ProviderRelay, ProviderRelayDialect } from "./server.js";
4
+ export { startSwitchingGatewayProxy } from "./switching-proxy.js";
5
+ export type { SwitchingGatewayProxy } from "./switching-proxy.js";
6
+ export { joinPath, ModelRoutedBackend, OpenAiBackend } from "./backend.js";
7
+ export type { Backend, BackendModelRoute, BackendRequestOptions, RequestAttributionUpdate, ModelRoutedBackendOptions, OpenAiBackendOptions } from "./backend.js";
8
+ export { AnthropicBackend, CodexResponsesBackend, GoogleGenAiBackend } from "./provider-backends.js";
9
+ export type { ProviderBackendOptions, ProviderTransport } from "./provider-backends.js";
10
+ export { CatalogBackend, isSubscriptionProvider, NoModelAvailableError, normalizeRouterConfigAliases, parseRouterConfig, routerConfigSchema, splitNamespacedModel, UnknownModelError } from "./router.js";
11
+ export type { CatalogBackendOptions, CatalogModelInfo, ProviderPolicy, RouterConfig, } from "./router.js";
12
+ export { API_PROVIDER_IDS, ApiProviderSource, parseDiscoveredModels, parseReasoningCapabilities, PROVIDER_IDS, SUBSCRIPTION_PROVIDER_IDS } from "./provider-source.js";
13
+ export type { ApiProviderId, ApiProviderSourceOptions, DiscoveredModel, ProviderId, ProviderSource, ProviderSourceTransport, SubscriptionProviderId } from "./provider-source.js";
14
+ export { endpointHealthProbe, probeEndpointHealth, providerAuthHeaders } from "./endpoint-health.js";
15
+ export type { AccountEndpointConfig, EndpointHealthProbe, EndpointHealthProbePlan, EndpointHealthResult, ModelEndpointConfig, UrlEndpointConfig } from "./endpoint-health.js";
16
+ export { CapacityPool } from "./capacity-pool.js";
17
+ export type { CapacityLease, CapacityPoolMember, CapacityPoolOptions, CapacityPoolStrategy } from "./capacity-pool.js";
18
+ export { effectiveModel, isStream, withDefaultModel } from "./adapters/chat.js";
19
+ export { isCursorChatBody, translateCursorRequest } from "./adapters/cursor.js";
20
+ export { anthropicModelsResponse, anthropicToChat, CLAUDE_ALIAS_PREFIX, chatToAnthropicMessage, claudeModelAlias, countTokensEstimate, handleAnthropicMessages, handleCountTokens, mapStopReason, openAiSseToAnthropic } from "./adapters/anthropic.js";
21
+ export type { AnthropicRequest } from "./adapters/anthropic.js";
22
+ export { chatToResponses, customToolNames, handleResponses, openAiSseToResponses, responsesToChat, responsesToolRegistry } from "./adapters/responses.js";
23
+ export type { ResponsesRequest, ResponsesToolKind, ResponsesToolRegistry } from "./adapters/responses.js";
24
+ export { MAX_WEB_SEARCHES_PER_TURN, resolveWebSearchExecutor } from "./adapters/web-search.js";
25
+ export type { WebSearchDialect, WebSearchExecutor, WebSearchOutcome } from "./adapters/web-search.js";
26
+ export { DIALECT_DROPPED_ATTRIBUTE, droppedField, resetDroppedFieldWarnings, withDroppedFieldSpan } from "./adapters/dropped.js";
27
+ export type { DialectName, DroppedFieldSpan } from "./adapters/dropped.js";
28
+ export { ACP_PROTOCOL_VERSION, runAcpAgent } from "./acp-agent.js";
29
+ export type { AcpAgentOptions, AcpRunner, AcpRunnerInput, AcpRunnerResult } from "./acp-agent.js";
30
+ export { ACP_REGISTRY_URL, fetchAcpRegistry, installAcpAdapters } from "./acp-registry.js";
31
+ export type { AcpRegistry, AcpRegistryAgent, AcpRegistryFetcher, InstallAcpAdaptersOptions, InstalledAcpAdapter } from "./acp-registry.js";
32
+ export { DEFAULT_MODEL_PRICING, estimateCost, formatUsd, lookupPricing, meterCall, parseUsage, parseUsageFromSse } from "./cost.js";
33
+ export type { CallCostRecord, ModelPricing, ProviderCostMetadata, TokenUsage } from "./cost.js";
34
+ export { buildModelCallRecord, MODEL_CALL_ID_HEADER, modelCallId, readProducerVersion, resolveProducerGitSha, responseBodyHash, UNKNOWN_GIT_SHA } from "./provenance.js";
35
+ export type { GatewayDialect, ModelCallRecord, ModelGatewayCallContext, ModelGatewayCallResult, ProvenanceSink } from "./provenance.js";
36
+ export { authorizedRequest } from "./auth.js";
37
+ export { errorEvent, finishChunk, noticeChunk, reasoningChunk, sseResponse } from "./sse-wire.js";
38
+ export { ChatStreamAssembler } from "./sse/chat-assembler.js";
39
+ export type { AssembledToolCall } from "./sse/chat-assembler.js";
40
+ export { decodeBufferedSse, SseDecoder, SseParseError } from "./sse/parse.js";