@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,140 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { validateAnthropicRequest, validateChatRequest, validateCountTokensRequest, validateResponsesRequest } from "../adapters/validate.js";
4
+ const chatOk = { model: "m", messages: [{ role: "user", content: "hi" }] };
5
+ test("chat door rejects structurally hostile bodies with an OpenAI 400 envelope", () => {
6
+ const hostile = [
7
+ ["non-object body", "hello"],
8
+ ["array body", [1, 2, 3]],
9
+ ["empty body", {}],
10
+ ["null messages", { model: "m", messages: null }],
11
+ ["string messages", { model: "m", messages: "hi" }],
12
+ ["empty messages", { model: "m", messages: [] }],
13
+ ["message without role", { model: "m", messages: [{ content: "x" }] }],
14
+ ["numeric content", { model: "m", messages: [{ role: "user", content: 42 }] }],
15
+ ["tool message without call id", { model: "m", messages: [{ role: "tool", content: "result" }] }],
16
+ ["string tool_calls", { model: "m", messages: [{ role: "assistant", content: null, tool_calls: "read" }] }],
17
+ ["malformed tool arguments", {
18
+ model: "m",
19
+ messages: [{
20
+ role: "assistant",
21
+ content: null,
22
+ tool_calls: [{ id: "c", type: "function", function: { name: "read", arguments: '{"bad"' } }]
23
+ }]
24
+ }],
25
+ ["array model", { model: ["m"], messages: chatOk.messages }],
26
+ ["object model", { model: { id: "m" }, messages: chatOk.messages }],
27
+ ["string stream", { ...chatOk, stream: "yes" }],
28
+ ["string tools", { ...chatOk, tools: "read" }]
29
+ ];
30
+ for (const [name, body] of hostile) {
31
+ const rejection = validateChatRequest(body);
32
+ assert.notEqual(rejection, undefined, `${name} must be rejected`);
33
+ assert.equal(rejection?.status, 400, name);
34
+ const envelope = rejection?.body;
35
+ assert.equal(envelope.error.type, "invalid_request_error", name);
36
+ assert.ok(envelope.error.message.length > 0, name);
37
+ }
38
+ });
39
+ test("chat door accepts every shape a real provider accepts", () => {
40
+ const fine = [
41
+ ["plain turn", chatOk],
42
+ ["no model (default injection)", { messages: chatOk.messages }],
43
+ ["null content", { model: "m", messages: [{ role: "user", content: null }] }],
44
+ ["content parts", { model: "m", messages: [{ role: "user", content: [{ type: "text", text: "x" }] }] }],
45
+ ["unknown role string", { model: "m", messages: [{ role: "developer", content: "x" }] }],
46
+ ["stream null", { ...chatOk, stream: null }],
47
+ ["stream true", { ...chatOk, stream: true }],
48
+ ["tools array", { ...chatOk, tools: [] }],
49
+ ["extra unknown fields", { ...chatOk, extension: { mode: "custom" }, metadata: { a: 1 } }]
50
+ ];
51
+ for (const [name, body] of fine) {
52
+ assert.equal(validateChatRequest(body), undefined, `${name} must pass`);
53
+ }
54
+ });
55
+ test("anthropic door rejects hostile bodies with the Anthropic 400 envelope", () => {
56
+ const hostile = [
57
+ ["empty body", {}],
58
+ ["null messages", { model: "c", max_tokens: 10, messages: null }],
59
+ ["empty messages", { model: "c", max_tokens: 10, messages: [] }],
60
+ ["array model", { model: ["c"], max_tokens: 10, messages: [{ role: "user", content: "x" }] }],
61
+ ["string max_tokens", { model: "c", max_tokens: "lots", messages: [{ role: "user", content: "x" }] }],
62
+ ["numeric system", { model: "c", max_tokens: 10, system: 42, messages: [{ role: "user", content: "x" }] }],
63
+ ["string tools", { model: "c", max_tokens: 10, tools: "hammer", messages: [{ role: "user", content: "x" }] }]
64
+ ];
65
+ for (const [name, body] of hostile) {
66
+ const rejection = validateAnthropicRequest(body);
67
+ assert.equal(rejection?.status, 400, name);
68
+ const envelope = rejection?.body;
69
+ assert.equal(envelope.type, "error", name);
70
+ assert.equal(envelope.error.type, "invalid_request_error", name);
71
+ }
72
+ });
73
+ test("anthropic door accepts real Claude Code shapes", () => {
74
+ const fine = [
75
+ ["plain", { model: "c", max_tokens: 100, messages: [{ role: "user", content: "x" }] }],
76
+ ["block content + system blocks", {
77
+ model: "c",
78
+ max_tokens: 100,
79
+ system: [{ type: "text", text: "sys" }],
80
+ messages: [{ role: "user", content: [{ type: "text", text: "x" }] }]
81
+ }],
82
+ ["no max_tokens (gateway is tolerant)", { model: "c", messages: [{ role: "user", content: "x" }] }],
83
+ ["null thinking/metadata", {
84
+ model: "c",
85
+ max_tokens: 100,
86
+ thinking: null,
87
+ metadata: null,
88
+ messages: [{ role: "user", content: "x" }]
89
+ }]
90
+ ];
91
+ for (const [name, body] of fine) {
92
+ assert.equal(validateAnthropicRequest(body), undefined, `${name} must pass`);
93
+ }
94
+ });
95
+ test("count_tokens requires a messages array but no minimum length", () => {
96
+ assert.equal(validateCountTokensRequest({ messages: [] }), undefined);
97
+ assert.equal(validateCountTokensRequest({ messages: [{ role: "user", content: "x" }] }), undefined);
98
+ assert.equal(validateCountTokensRequest({})?.status, 400);
99
+ assert.equal(validateCountTokensRequest({ messages: "hi" })?.status, 400);
100
+ assert.equal(validateCountTokensRequest({ messages: [42] })?.status, 400);
101
+ assert.equal(validateCountTokensRequest({ messages: [{ content: "missing role" }] })?.status, 400);
102
+ assert.equal(validateCountTokensRequest({ messages: [{ role: "assistant", content: null }] })
103
+ ?.status, 400);
104
+ assert.equal(validateCountTokensRequest({ messages: [{ role: "user", content: { text: "wrong shape" } }] })
105
+ ?.status, 400);
106
+ });
107
+ test("token limits must be positive integers at every wire door", () => {
108
+ assert.equal(validateChatRequest({ ...chatOk, max_tokens: -1 })?.status, 400);
109
+ assert.equal(validateChatRequest({ ...chatOk, max_completion_tokens: 1.5 })?.status, 400);
110
+ assert.equal(validateAnthropicRequest({
111
+ model: "c",
112
+ messages: [{ role: "user", content: "x" }],
113
+ max_tokens: 0
114
+ })?.status, 400);
115
+ assert.equal(validateResponsesRequest({ model: "m", input: "x", max_output_tokens: -1 })?.status, 400);
116
+ });
117
+ test("responses door requires a usable input and string model", () => {
118
+ const hostile = [
119
+ ["empty body", {}],
120
+ ["numeric input", { model: "m", input: 42 }],
121
+ ["empty input array", { model: "m", input: [] }],
122
+ ["non-object input item", { model: "m", input: ["hi"] }],
123
+ ["object model", { model: { a: 1 }, input: "hi" }],
124
+ ["string tools", { model: "m", input: "hi", tools: "hammer" }]
125
+ ];
126
+ for (const [name, body] of hostile) {
127
+ const rejection = validateResponsesRequest(body);
128
+ assert.equal(rejection?.status, 400, name);
129
+ assert.equal((rejection?.body).error.type, "invalid_request_error", name);
130
+ }
131
+ const fine = [
132
+ ["string input", { model: "m", input: "hi" }],
133
+ ["item input", { model: "m", input: [{ type: "message", role: "user", content: "hi" }] }],
134
+ ["codex nulls", { model: "m", input: "hi", reasoning: null, text: null, tool_choice: null }],
135
+ ["unknown item types (dropped downstream)", { model: "m", input: [{ type: "quantum" }] }]
136
+ ];
137
+ for (const [name, body] of fine) {
138
+ assert.equal(validateResponsesRequest(body), undefined, `${name} must pass`);
139
+ }
140
+ });
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@velum-labs/routekit-gateway",
3
+ "private": false,
4
+ "version": "0.9.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/velum-labs/handoffkit.git",
8
+ "directory": "packages/model-gateway"
9
+ },
10
+ "description": "Product-neutral model router, provider egress, wire dialect translation, streaming, catalog, and call provenance.",
11
+ "license": "Apache-2.0",
12
+ "type": "module",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "LICENSE"
22
+ ],
23
+ "publishConfig": {
24
+ "registry": "https://registry.npmjs.org",
25
+ "access": "public",
26
+ "provenance": true
27
+ },
28
+ "dependencies": {
29
+ "zod": "4.4.3",
30
+ "@velum-labs/routekit-contracts": "0.9.0",
31
+ "@velum-labs/routekit-tracing": "0.9.0",
32
+ "@velum-labs/routekit-runtime": "0.9.0",
33
+ "@velum-labs/routekit-registry": "0.9.0"
34
+ },
35
+ "keywords": [
36
+ "routekit",
37
+ "llm",
38
+ "coding-agent",
39
+ "gateway",
40
+ "openai-compatible",
41
+ "streaming"
42
+ ],
43
+ "scripts": {
44
+ "build": "tsc -b",
45
+ "clean": "tsc -b --clean",
46
+ "test": "node --test \"dist/test/*.test.js\""
47
+ }
48
+ }