@velum-labs/routekit-gateway 0.9.6 → 0.9.7

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.
@@ -49,7 +49,7 @@ export function isCursorChatBody(body) {
49
49
  * model catalog.
50
50
  */
51
51
  export function cursorModelAliasId(id) {
52
- return id.replace("/", "-");
52
+ return id.replaceAll("/", "-");
53
53
  }
54
54
  /**
55
55
  * Resolve Cursor's dash-separated spelling back to a served namespaced id.
package/dist/router.d.ts CHANGED
@@ -94,6 +94,7 @@ export declare const routerConfigSchema: z.ZodObject<{
94
94
  }, z.core.$strict>>;
95
95
  }, z.core.$strict>;
96
96
  defaultModel: z.ZodOptional<z.ZodString>;
97
+ modelAliases: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
97
98
  reasoningCapabilities: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
98
99
  status: z.ZodDefault<z.ZodEnum<{
99
100
  unknown: "unknown";
package/dist/router.js CHANGED
@@ -96,6 +96,7 @@ export const routerConfigSchema = z
96
96
  })
97
97
  .strict(),
98
98
  defaultModel: z.string().min(3).optional(),
99
+ modelAliases: z.record(z.string().min(1), z.string().min(3)).optional(),
99
100
  reasoningCapabilities: z
100
101
  .record(z.string().min(3), reasoningCapabilityOverrideSchema)
101
102
  .optional()
@@ -143,6 +144,15 @@ export function parseRouterConfig(value) {
143
144
  throw new Error(`default model provider "${selected.provider}" is not configured`);
144
145
  }
145
146
  }
147
+ for (const [alias, target] of Object.entries(config.modelAliases ?? {})) {
148
+ if (alias.includes("/")) {
149
+ throw new Error(`model alias "${alias}" must not contain "/"; aliases exist to give namespaced models slash-free names`);
150
+ }
151
+ const selected = splitNamespacedModel(target);
152
+ if (config.providers[selected.provider] === undefined) {
153
+ throw new Error(`model alias "${alias}" targets "${target}" but provider "${selected.provider}" is not configured`);
154
+ }
155
+ }
146
156
  return config;
147
157
  }
148
158
  function routeBilling(provider) {
@@ -236,6 +246,16 @@ export class CatalogBackend {
236
246
  });
237
247
  }
238
248
  }
249
+ for (const [alias, target] of Object.entries(config.modelAliases ?? {})) {
250
+ const entry = entries.get(target);
251
+ if (entry === undefined) {
252
+ throw new Error(`model alias "${alias}" targets "${target}", which no configured provider serves`);
253
+ }
254
+ if (entries.has(alias)) {
255
+ throw new Error(`model alias "${alias}" collides with a served model id`);
256
+ }
257
+ entries.set(alias, { ...entry, publicId: alias });
258
+ }
239
259
  const first = entries.keys().next().value;
240
260
  const defaultModel = config.defaultModel ?? first;
241
261
  if (defaultModel === undefined) {
@@ -51,7 +51,12 @@ test("Cursor model aliases respell namespaced ids with dashes", () => {
51
51
  assert.equal(cursorModelAliasId("claude-code/claude-fable-5"), "claude-code-claude-fable-5");
52
52
  assert.equal(cursorModelAliasId("openai/gpt-4o"), "openai-gpt-4o");
53
53
  assert.equal(cursorModelAliasId("route-primary"), "route-primary");
54
+ assert.equal(cursorModelAliasId("openrouter/moonshotai/kimi-k2-thinking"), "openrouter-moonshotai-kimi-k2-thinking", "every slash is respelled, not just the namespace separator");
54
55
  const served = ["claude-code/claude-fable-5", "openai/gpt-4o", "route-primary"];
56
+ assert.equal(resolveCursorModelAlias("openrouter-moonshotai-kimi-k2-thinking", [
57
+ ...served,
58
+ "openrouter/moonshotai/kimi-k2-thinking"
59
+ ]), "openrouter/moonshotai/kimi-k2-thinking");
55
60
  assert.equal(resolveCursorModelAlias("claude-code-claude-fable-5", served), "claude-code/claude-fable-5");
56
61
  assert.equal(resolveCursorModelAlias("openai-gpt-4o", served), "openai/gpt-4o");
57
62
  // Served-as-spelled ids and unknown names never rewrite.
@@ -156,6 +156,58 @@ test("catalog namespaces live models and strips the source before dispatch", asy
156
156
  });
157
157
  assert.equal(backend.modelInfo("openai/not-real"), undefined);
158
158
  });
159
+ test("configured model aliases serve namespaced models under slash-free names", async () => {
160
+ const calls = [];
161
+ const backend = await CatalogBackend.create({
162
+ config: {
163
+ providers: { "claude-code": {} },
164
+ modelAliases: { "velum-fable-5": "claude-code/claude-fable-5" }
165
+ },
166
+ sources: {
167
+ "claude-code": fakeSource("claude-code", [{ id: "claude-fable-5" }], calls)
168
+ }
169
+ });
170
+ assert.deepEqual(backend.listModelIds(), [
171
+ "claude-code/claude-fable-5",
172
+ "velum-fable-5"
173
+ ]);
174
+ assert.equal(backend.servesModel("velum-fable-5"), true);
175
+ assert.deepEqual(backend.resolveModelRoute("velum-fable-5"), {
176
+ publicId: "velum-fable-5",
177
+ nativeId: "claude-fable-5",
178
+ provider: "claude-code"
179
+ });
180
+ assert.equal(backend.modelInfo("velum-fable-5")?.id, "velum-fable-5");
181
+ assert.equal(backend.modelInfo("velum-fable-5")?.nativeModel, "claude-fable-5");
182
+ await backend.chat({ model: "velum-fable-5", messages: [] });
183
+ assert.deepEqual(calls, [
184
+ { source: "claude-code", model: "claude-fable-5" }
185
+ ]);
186
+ });
187
+ test("model aliases reject bad shapes and unknown targets", async () => {
188
+ assert.throws(() => parseRouterConfig({
189
+ providers: { openai: {} },
190
+ modelAliases: { "bad/alias": "openai/gpt-5.5" }
191
+ }), /must not contain "\/"/);
192
+ assert.throws(() => parseRouterConfig({
193
+ providers: { openai: {} },
194
+ modelAliases: { "velum-fable-5": "claude-code/claude-fable-5" }
195
+ }), /provider "claude-code" is not configured/);
196
+ await assert.rejects(CatalogBackend.create({
197
+ config: {
198
+ providers: { openai: {} },
199
+ modelAliases: { "velum-gpt": "openai/not-discovered" }
200
+ },
201
+ sources: { openai: fakeSource("openai", [{ id: "gpt-5.5" }]) }
202
+ }), /targets "openai\/not-discovered", which no configured provider serves/);
203
+ await assert.rejects(CatalogBackend.create({
204
+ config: {
205
+ providers: { openai: {} },
206
+ modelAliases: { "openai/gpt-5.5": "openai/gpt-5.5" }
207
+ },
208
+ sources: { openai: fakeSource("openai", [{ id: "gpt-5.5" }]) }
209
+ }), /must not contain "\/"/);
210
+ });
159
211
  test("model info exhaustively classifies subscription and proxy billing", async () => {
160
212
  const backend = await CatalogBackend.create({
161
213
  config: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@velum-labs/routekit-gateway",
3
3
  "private": false,
4
- "version": "0.9.6",
4
+ "version": "0.9.7",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/velum-labs/handoffkit.git",
@@ -27,10 +27,10 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "zod": "4.4.3",
30
- "@velum-labs/routekit-contracts": "0.9.6",
31
- "@velum-labs/routekit-registry": "0.9.6",
32
- "@velum-labs/routekit-runtime": "0.9.6",
33
- "@velum-labs/routekit-tracing": "0.9.6"
30
+ "@velum-labs/routekit-contracts": "0.9.7",
31
+ "@velum-labs/routekit-tracing": "0.9.7",
32
+ "@velum-labs/routekit-runtime": "0.9.7",
33
+ "@velum-labs/routekit-registry": "0.9.7"
34
34
  },
35
35
  "keywords": [
36
36
  "routekit",