@vellumai/vellum-gateway 0.10.9-staging.1 → 0.10.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/vellum-gateway",
3
- "version": "0.10.9-staging.1",
3
+ "version": "0.10.9",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,4 +1,4 @@
1
- import { describe, test, expect, mock } from "bun:test";
1
+ import { afterAll, beforeAll, describe, test, expect, mock } from "bun:test";
2
2
  import type { GatewayConfig } from "../config.js";
3
3
  import { initSigningKey, mintToken } from "../auth/token-service.js";
4
4
  import { CURRENT_POLICY_EPOCH } from "../auth/policy.js";
@@ -6,6 +6,7 @@ import type { CredentialCache } from "../credential-cache.js";
6
6
  import {
7
7
  createSpeechRelayUpgradeHandler,
8
8
  getSpeechRelayWebsocketHandlers,
9
+ velayOriginForPlatformHost,
9
10
  type SpeechRelaySocketData,
10
11
  } from "../http/routes/speech-relay-websocket.js";
11
12
  import { VELAY_FORWARDED_HEADER } from "../velay/bridge-utils.js";
@@ -81,6 +82,126 @@ async function bodyOf(
81
82
 
82
83
  const TOKEN = mintDaemonServiceToken();
83
84
 
85
+ function makeKeyedCredentials(
86
+ values: Record<string, string | undefined>,
87
+ ): CredentialCache {
88
+ return {
89
+ get: mock(async (key: string) => values[key]),
90
+ } as unknown as CredentialCache;
91
+ }
92
+
93
+ describe("velay origin derivation", () => {
94
+ // getPlatformBaseUrl falls back to this env var; pin it absent so the
95
+ // missing-credential cases below are deterministic on dev machines.
96
+ const savedPlatformUrl = process.env.VELLUM_PLATFORM_URL;
97
+ beforeAll(() => {
98
+ delete process.env.VELLUM_PLATFORM_URL;
99
+ });
100
+ afterAll(() => {
101
+ if (savedPlatformUrl !== undefined) {
102
+ process.env.VELLUM_PLATFORM_URL = savedPlatformUrl;
103
+ }
104
+ });
105
+
106
+ test("velayOriginForPlatformHost follows the deployment naming convention", () => {
107
+ expect(velayOriginForPlatformHost("platform.vellum.ai")).toBe(
108
+ "https://velay.vellum.ai",
109
+ );
110
+ expect(velayOriginForPlatformHost("staging-platform.vellum.ai")).toBe(
111
+ "https://velay-staging.vellum.ai",
112
+ );
113
+ expect(velayOriginForPlatformHost("dev-platform.vellum.ai")).toBe(
114
+ "https://velay-dev.vellum.ai",
115
+ );
116
+ expect(velayOriginForPlatformHost("localhost")).toBeNull();
117
+ expect(velayOriginForPlatformHost("example.com")).toBeNull();
118
+ expect(
119
+ velayOriginForPlatformHost("staging-platform.vellum.ai.evil.com"),
120
+ ).toBeNull();
121
+ });
122
+
123
+ test("derives the velay origin from the stored platform base URL when VELAY_BASE_URL is unset", async () => {
124
+ const server = makeFakeServer();
125
+ const handler = createSpeechRelayUpgradeHandler(
126
+ makeConfig({ velayBaseUrl: undefined }),
127
+ "stt",
128
+ {
129
+ credentials: makeKeyedCredentials({
130
+ "credential/vellum/assistant_api_key": "vk-1",
131
+ "credential/vellum/platform_base_url":
132
+ "https://staging-platform.vellum.ai",
133
+ }),
134
+ },
135
+ );
136
+ const req = new Request(
137
+ `http://127.0.0.1:7830/v1/speech/stt/stream?key=${TOKEN}`,
138
+ { headers: { upgrade: "websocket" } },
139
+ );
140
+
141
+ expect(await handler(req, server)).toBeUndefined();
142
+ const data = (server.upgrade as unknown as { mock: { calls: unknown[][] } })
143
+ .mock.calls[0]![1] as { data: SpeechRelaySocketData };
144
+ expect(new URL(data.data.upstreamWsUrl).origin).toBe(
145
+ "wss://velay-staging.vellum.ai",
146
+ );
147
+ expect(new URL(data.data.upstreamHttpUrl).origin).toBe(
148
+ "https://velay-staging.vellum.ai",
149
+ );
150
+ });
151
+
152
+ test("falls back to prod velay for an off-convention or missing platform base URL", async () => {
153
+ for (const platformBaseUrl of [
154
+ "http://localhost:8000",
155
+ "not a url",
156
+ undefined,
157
+ ]) {
158
+ const server = makeFakeServer();
159
+ const handler = createSpeechRelayUpgradeHandler(
160
+ makeConfig({ velayBaseUrl: undefined }),
161
+ "stt",
162
+ {
163
+ credentials: makeKeyedCredentials({
164
+ "credential/vellum/assistant_api_key": "vk-1",
165
+ "credential/vellum/platform_base_url": platformBaseUrl,
166
+ }),
167
+ },
168
+ );
169
+ const req = new Request(
170
+ `http://127.0.0.1:7830/v1/speech/stt/stream?key=${TOKEN}`,
171
+ { headers: { upgrade: "websocket" } },
172
+ );
173
+
174
+ expect(await handler(req, server)).toBeUndefined();
175
+ const data = (
176
+ server.upgrade as unknown as { mock: { calls: unknown[][] } }
177
+ ).mock.calls[0]![1] as { data: SpeechRelaySocketData };
178
+ expect(new URL(data.data.upstreamWsUrl).origin).toBe(
179
+ "wss://velay.vellum.ai",
180
+ );
181
+ }
182
+ });
183
+
184
+ test("an explicit VELAY_BASE_URL wins over the stored platform base URL", async () => {
185
+ const server = makeFakeServer();
186
+ const handler = createSpeechRelayUpgradeHandler(makeConfig(), "stt", {
187
+ credentials: makeKeyedCredentials({
188
+ "credential/vellum/assistant_api_key": "vk-1",
189
+ "credential/vellum/platform_base_url":
190
+ "https://staging-platform.vellum.ai",
191
+ }),
192
+ });
193
+ const req = new Request(
194
+ `http://127.0.0.1:7830/v1/speech/stt/stream?key=${TOKEN}`,
195
+ { headers: { upgrade: "websocket" } },
196
+ );
197
+
198
+ expect(await handler(req, server)).toBeUndefined();
199
+ const data = (server.upgrade as unknown as { mock: { calls: unknown[][] } })
200
+ .mock.calls[0]![1] as { data: SpeechRelaySocketData };
201
+ expect(new URL(data.data.upstreamWsUrl).origin).toBe("wss://velay.test");
202
+ });
203
+ });
204
+
84
205
  describe("createSpeechRelayUpgradeHandler — gate", () => {
85
206
  test("upgrades a daemon service connection and strips the key param", async () => {
86
207
  const server = makeFakeServer();
@@ -178,14 +178,6 @@
178
178
  "description": "Show a live HUD in the top-right of the conversation with scroll geometry, velocity, update rate, and anchor-preserver activity. Developer diagnostic for investigating scroll jank.",
179
179
  "defaultEnabled": false
180
180
  },
181
- {
182
- "id": "coding-agents-panel",
183
- "scope": "client",
184
- "key": "coding-agents-panel",
185
- "label": "Coding Agents Panel",
186
- "description": "Gate native macOS Coding Agents panel entry points and inline ACP session deep links.",
187
- "defaultEnabled": false
188
- },
189
181
  {
190
182
  "id": "compaction-playground",
191
183
  "scope": "assistant",
@@ -311,7 +303,7 @@
311
303
  "scope": "assistant",
312
304
  "key": "memory-concept-graph",
313
305
  "label": "Memory concept graph",
314
- "description": "Gates the Obsidian-style memory concept graph on the assistant identity page and the backend-agnostic /memory-graph + /memory-graph-node routes that feed it. When off, the identity page shows the skills constellation instead. Only produces a graph on memory-v3 backends. Default off.",
306
+ "description": "Gates the Obsidian-style memory concept graph on its own Memory tab (/assistant/memory) and the backend-agnostic /memory-graph + /memory-graph-node routes that feed it. When off, the Memory tab is hidden; the identity page keeps the skills constellation. Only produces a graph on memory-v3 backends. Default off.",
315
307
  "defaultEnabled": false
316
308
  },
317
309
  {
@@ -27,6 +27,7 @@ import type { GatewayConfig } from "../../config.js";
27
27
  import type { CredentialCache } from "../../credential-cache.js";
28
28
  import { credentialKey } from "../../credential-key.js";
29
29
  import { getLogger } from "../../logger.js";
30
+ import { getPlatformBaseUrl } from "../../platform-url.js";
30
31
  import { VELAY_FORWARDED_HEADER } from "../../velay/bridge-utils.js";
31
32
  import { requestHasVelayBridgeAuth } from "../../velay/bridge-auth.js";
32
33
 
@@ -108,17 +109,58 @@ function jsonError(status: number, code: string, detail: string): Response {
108
109
  });
109
110
  }
110
111
 
111
- function velaySpeechUrls(
112
+ /**
113
+ * Map a Vellum platform host onto its environment's velay origin, following
114
+ * the deployment naming convention (`platform.vellum.ai` → `velay.vellum.ai`,
115
+ * `{env}-platform.vellum.ai` → `velay-{env}.vellum.ai`). Returns null for
116
+ * hosts outside that convention (localhost, custom domains).
117
+ */
118
+ export function velayOriginForPlatformHost(host: string): string | null {
119
+ if (host === "platform.vellum.ai") {
120
+ return "https://velay.vellum.ai";
121
+ }
122
+ const match = /^([a-z0-9-]+)-platform\.vellum\.ai$/.exec(host);
123
+ return match ? `https://velay-${match[1]}.vellum.ai` : null;
124
+ }
125
+
126
+ /**
127
+ * Resolve the velay origin for this dial. An explicit VELAY_BASE_URL always
128
+ * wins; otherwise derive it from the stored platform base URL so a non-prod
129
+ * assistant's key is presented to its own environment's velay rather than
130
+ * prod (which would reject it with `invalid_key`).
131
+ */
132
+ async function resolveVelayBaseUrl(
112
133
  config: GatewayConfig,
134
+ deps: SpeechRelayDeps,
135
+ ): Promise<string> {
136
+ if (config.velayBaseUrl) {
137
+ return config.velayBaseUrl;
138
+ }
139
+ const platformBaseUrl = await getPlatformBaseUrl(deps.credentials);
140
+ if (platformBaseUrl) {
141
+ try {
142
+ const derived = velayOriginForPlatformHost(
143
+ new URL(platformBaseUrl).hostname,
144
+ );
145
+ if (derived) {
146
+ return derived;
147
+ }
148
+ } catch {
149
+ // Unparseable platform URL — fall through to the prod default.
150
+ }
151
+ }
152
+ return DEFAULT_VELAY_BASE_URL;
153
+ }
154
+
155
+ function velaySpeechUrls(
156
+ velayBaseUrl: string,
113
157
  operation: SpeechRelayOperation,
114
158
  params: URLSearchParams,
115
159
  ): { wsUrl: string; httpUrl: string } {
116
160
  // The velay tunnel client accepts ws(s):// bases too — normalize to the
117
161
  // http(s) twin first so the probe URL is always fetchable, then derive
118
162
  // the ws(s) dial URL from that.
119
- const httpBase = (config.velayBaseUrl ?? DEFAULT_VELAY_BASE_URL)
120
- .replace(/\/+$/, "")
121
- .replace(/^ws/, "http");
163
+ const httpBase = velayBaseUrl.replace(/\/+$/, "").replace(/^ws/, "http");
122
164
  const query = params.toString();
123
165
  const path = `/v1/speech/${operation}/stream${query ? `?${query}` : ""}`;
124
166
  return {
@@ -221,7 +263,11 @@ export function createSpeechRelayUpgradeHandler(
221
263
  );
222
264
  }
223
265
  }
224
- const { wsUrl, httpUrl } = velaySpeechUrls(config, operation, params);
266
+ const { wsUrl, httpUrl } = velaySpeechUrls(
267
+ await resolveVelayBaseUrl(config, deps),
268
+ operation,
269
+ params,
270
+ );
225
271
 
226
272
  // Non-upgrade request: this is the daemon's dial-failure probe.
227
273
  // Replay the gate and hand back velay's own rejection.
@@ -123,6 +123,7 @@ const ASSISTANT_SUPPORTED_COMMAND_PATHS = [
123
123
  "db status",
124
124
  "db repair",
125
125
  "gateway",
126
+ "gateway status",
126
127
  "gateway logs",
127
128
  "gateway logs tail",
128
129
  "image-generation",