@ai-sdk/gateway
Advanced tools
+20
-0
| # @ai-sdk/gateway | ||
| ## 4.0.0-beta.109 | ||
| ### Patch Changes | ||
| - 15eb253: feat(gateway): mint short-lived client secrets for experimental realtime | ||
| `gateway.experimental_realtime.getToken()` now mints a single-use, short-lived | ||
| client secret (`vcst_`) via the Gateway's `POST /v1/realtime/client-secrets` | ||
| endpoint instead of returning the long-lived Gateway credential. The customer's | ||
| server calls `getToken()` and hands the returned token to the browser, which | ||
| opens the realtime WebSocket with it through the existing | ||
| `ai-gateway-auth.<token>` subprotocol — the API key / OIDC token never reaches | ||
| the client. `expiresAfterSeconds` is forwarded to the mint endpoint and the | ||
| returned `expiresAt` is surfaced on the result. | ||
| The server-environment guard moves from realtime model construction to minting: | ||
| the browser can now build the realtime event codec it needs to drive the | ||
| transport, while minting (which requires the Gateway credential) stays | ||
| server-side. | ||
| ## 4.0.0-beta.108 | ||
@@ -4,0 +24,0 @@ |
+1
-1
| { | ||
| "name": "@ai-sdk/gateway", | ||
| "private": false, | ||
| "version": "4.0.0-beta.108", | ||
| "version": "4.0.0-beta.109", | ||
| "type": "module", | ||
@@ -6,0 +6,0 @@ "license": "Apache-2.0", |
| import { | ||
| createJsonErrorResponseHandler, | ||
| createJsonResponseHandler, | ||
| loadOptionalSetting, | ||
| postJsonToApi, | ||
| withoutTrailingSlash, | ||
@@ -7,2 +10,3 @@ withUserAgentSuffix, | ||
| } from '@ai-sdk/provider-utils'; | ||
| import { z } from 'zod/v4'; | ||
| import { asGatewayError, GatewayAuthenticationError } from './errors'; | ||
@@ -223,2 +227,8 @@ import { | ||
| /** Response shape of `POST /v1/realtime/client-secrets`. `expiresAt` is epoch seconds. */ | ||
| const gatewayClientSecretResponseSchema = z.object({ | ||
| token: z.string(), | ||
| expiresAt: z.number().nullish(), | ||
| }); | ||
| /** | ||
@@ -283,2 +293,45 @@ * Create a remote provider instance. | ||
| // Mints a short-lived realtime client secret (`vcst_`) via the Gateway's | ||
| // `/v1/realtime/client-secrets` route, authenticated with the long-lived | ||
| // Gateway credential. Server-side only (asserted) — the credential never | ||
| // belongs in a browser; the browser receives only the minted token. The | ||
| // mint route lives at the gateway origin's `/v1/realtime/client-secrets`, | ||
| // not under the realtime `baseURL` path (which targets `/v4/ai`), so the | ||
| // URL is resolved against the origin. | ||
| const mintRealtimeClientSecret = async (params: { | ||
| modelId: string; | ||
| expiresAfterSeconds?: number; | ||
| }): Promise<{ token: string; expiresAt?: number }> => { | ||
| assertGatewayRealtimeServerEnvironment(); | ||
| const auth = await getRealtimeAuthToken(); | ||
| const headers = createAuthHeaders(auth); | ||
| const url = new URL('/v1/realtime/client-secrets', baseURL).toString(); | ||
| try { | ||
| const { value } = await postJsonToApi({ | ||
| url, | ||
| headers, | ||
| body: { | ||
| model: params.modelId, | ||
| ...(params.expiresAfterSeconds != null && { | ||
| expiresIn: params.expiresAfterSeconds, | ||
| }), | ||
| }, | ||
| successfulResponseHandler: createJsonResponseHandler( | ||
| gatewayClientSecretResponseSchema, | ||
| ), | ||
| failedResponseHandler: createJsonErrorResponseHandler({ | ||
| errorSchema: z.any(), | ||
| errorToMessage: data => data, | ||
| }), | ||
| fetch: options.fetch, | ||
| }); | ||
| return { | ||
| token: value.token, | ||
| ...(value.expiresAt != null && { expiresAt: value.expiresAt }), | ||
| }; | ||
| } catch (error) { | ||
| throw await asGatewayError(error, await parseAuthMethod(headers)); | ||
| } | ||
| }; | ||
| const createO11yHeaders = () => { | ||
@@ -474,11 +527,14 @@ const deploymentId = loadOptionalSetting({ | ||
| const createRealtimeModel = (modelId: GatewayRealtimeModelId) => { | ||
| assertGatewayRealtimeServerEnvironment(); | ||
| return new GatewayRealtimeModel(modelId, { | ||
| // No server-environment guard here: building the realtime model is just the | ||
| // event codec + WebSocket-config helper, which the browser legitimately | ||
| // needs to drive the transport with a server-minted client secret. The | ||
| // server-only boundary is enforced on minting itself | ||
| // (`mintRealtimeClientSecret`), which requires the Gateway credential. | ||
| const createRealtimeModel = (modelId: GatewayRealtimeModelId) => | ||
| new GatewayRealtimeModel(modelId, { | ||
| provider: 'gateway.realtime', | ||
| baseURL, | ||
| teamIdOrSlug: options.teamIdOrSlug, | ||
| getAuthToken: getRealtimeAuthToken, | ||
| createClientSecret: mintRealtimeClientSecret, | ||
| }); | ||
| }; | ||
| provider.experimental_realtime = Object.assign( | ||
@@ -488,4 +544,5 @@ (modelId: GatewayRealtimeModelId) => createRealtimeModel(modelId), | ||
| getToken: async (tokenOptions: RealtimeFactoryV4GetTokenOptions) => { | ||
| const model = createRealtimeModel(tokenOptions.model); | ||
| const secret = await model.doCreateClientSecret(); | ||
| const { model: modelId, ...secretOptions } = tokenOptions; | ||
| const model = createRealtimeModel(modelId); | ||
| const secret = await model.doCreateClientSecret(secretOptions); | ||
| return { | ||
@@ -535,5 +592,5 @@ token: secret.token, | ||
| throw new Error( | ||
| 'AI Gateway realtime models cannot be used in browsers yet. Use gateway.experimental_realtime from server-side code only.', | ||
| 'AI Gateway realtime client secrets must be minted server-side: minting needs your Gateway credential, which must never reach the browser. Call gateway.experimental_realtime.getToken() from your server and pass the returned token to the client.', | ||
| ); | ||
| } | ||
| } |
| import type { | ||
| Experimental_RealtimeModelV4 as RealtimeModelV4, | ||
| Experimental_RealtimeModelV4ClientEvent as RealtimeModelV4ClientEvent, | ||
| Experimental_RealtimeModelV4ClientSecretOptions as RealtimeModelV4ClientSecretOptions, | ||
| Experimental_RealtimeModelV4ClientSecretResult as RealtimeModelV4ClientSecretResult, | ||
@@ -15,9 +16,11 @@ Experimental_RealtimeModelV4ServerEvent as RealtimeModelV4ServerEvent, | ||
| /** | ||
| * Resolves the Gateway auth token used to authenticate the WebSocket upgrade | ||
| * (API key or Vercel OIDC token). | ||
| * Mints a short-lived client secret (`vcst_`) for this model via the | ||
| * Gateway's `/v1/realtime/client-secrets` endpoint. Implemented by the | ||
| * provider because minting requires the long-lived Gateway credential | ||
| * (API key / OIDC) and must run server-side. | ||
| */ | ||
| getAuthToken: () => PromiseLike<{ | ||
| token: string; | ||
| authMethod: 'api-key' | 'oidc'; | ||
| }>; | ||
| createClientSecret: (params: { | ||
| modelId: string; | ||
| expiresAfterSeconds?: number; | ||
| }) => PromiseLike<{ token: string; expiresAt?: number }>; | ||
| }; | ||
@@ -48,16 +51,24 @@ | ||
| /** | ||
| * Unlike providers with a dedicated ephemeral-secret endpoint (e.g. OpenAI), | ||
| * the Gateway v0 realtime path does not mint a new client secret. The returned | ||
| * token is the Gateway credential resolved by the provider (`apiKey`, | ||
| * `AI_GATEWAY_API_KEY`, or Vercel OIDC token) and the WebSocket upgrade is | ||
| * authenticated directly with that credential. The | ||
| * `RealtimeModelV4ClientSecretOptions` are therefore intentionally unused: | ||
| * `sessionConfig` is applied later via the normalized `session-update` event, | ||
| * and `expiresAfterSeconds` has no Gateway-side equivalent. | ||
| * Mints a single-use, short-lived client secret (`vcst_`) the browser uses to | ||
| * open the realtime WebSocket without ever holding the long-lived Gateway | ||
| * credential. The customer's server calls this (via | ||
| * `gateway.experimental_realtime.getToken`) and hands the returned token to | ||
| * the browser, which connects with it through the `ai-gateway-auth.<token>` | ||
| * subprotocol. `expiresAfterSeconds` is forwarded to the mint endpoint; | ||
| * `sessionConfig` is intentionally unused here — it is applied later via the | ||
| * normalized `session-update` event. | ||
| */ | ||
| async doCreateClientSecret(): Promise<RealtimeModelV4ClientSecretResult> { | ||
| const { token } = await this.config.getAuthToken(); | ||
| async doCreateClientSecret( | ||
| options?: RealtimeModelV4ClientSecretOptions, | ||
| ): Promise<RealtimeModelV4ClientSecretResult> { | ||
| const secret = await this.config.createClientSecret({ | ||
| modelId: this.modelId, | ||
| ...(options?.expiresAfterSeconds != null && { | ||
| expiresAfterSeconds: options.expiresAfterSeconds, | ||
| }), | ||
| }); | ||
| return { | ||
| token, | ||
| token: secret.token, | ||
| url: toGatewayRealtimeUrl(this.config.baseURL, this.modelId), | ||
| ...(secret.expiresAt != null && { expiresAt: secret.expiresAt }), | ||
| }; | ||
@@ -64,0 +75,0 @@ } |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
521222
1.97%7444
1.5%99
4.21%