🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@ai-sdk/gateway

Package Overview
Dependencies
Maintainers
3
Versions
569
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ai-sdk/gateway - npm Package Compare versions

Comparing version
4.0.17
to
4.0.19
+30
-9
dist/index.d.ts
import { LanguageModelV4, ProviderV4, EmbeddingModelV4, ImageModelV4, Experimental_VideoModelV4, RerankingModelV4, SpeechModelV4, TranscriptionModelV4, Experimental_RealtimeFactoryV4, TypeValidationError } from '@ai-sdk/provider';
import * as _ai_sdk_provider_utils from '@ai-sdk/provider-utils';
import { FetchFunction, InferSchema } from '@ai-sdk/provider-utils';
import { FetchFunction, WebSocketConstructor, InferSchema } from '@ai-sdk/provider-utils';

@@ -8,3 +8,4 @@ type GatewayModelId = 'alibaba/qwen-3-14b' | 'alibaba/qwen-3-235b' | 'alibaba/qwen-3-30b' | 'alibaba/qwen-3-32b' | 'alibaba/qwen-3.6-max-preview' | 'alibaba/qwen3-235b-a22b-thinking' | 'alibaba/qwen3-coder' | 'alibaba/qwen3-coder-30b-a3b' | 'alibaba/qwen3-coder-next' | 'alibaba/qwen3-coder-plus' | 'alibaba/qwen3-max' | 'alibaba/qwen3-max-preview' | 'alibaba/qwen3-max-thinking' | 'alibaba/qwen3-next-80b-a3b-instruct' | 'alibaba/qwen3-next-80b-a3b-thinking' | 'alibaba/qwen3-vl-235b-a22b-instruct' | 'alibaba/qwen3-vl-instruct' | 'alibaba/qwen3-vl-thinking' | 'alibaba/qwen3.5-flash' | 'alibaba/qwen3.5-plus' | 'alibaba/qwen3.6-27b' | 'alibaba/qwen3.6-plus' | 'alibaba/qwen3.7-max' | 'alibaba/qwen3.7-plus' | 'amazon/nova-2-lite' | 'amazon/nova-lite' | 'amazon/nova-micro' | 'amazon/nova-pro' | 'anthropic/claude-3-haiku' | 'anthropic/claude-3.5-haiku' | 'anthropic/claude-fable-5' | 'anthropic/claude-haiku-4.5' | 'anthropic/claude-opus-4' | 'anthropic/claude-opus-4.1' | 'anthropic/claude-opus-4.5' | 'anthropic/claude-opus-4.6' | 'anthropic/claude-opus-4.7' | 'anthropic/claude-opus-4.8' | 'anthropic/claude-sonnet-4' | 'anthropic/claude-sonnet-4.5' | 'anthropic/claude-sonnet-4.6' | 'anthropic/claude-sonnet-5' | 'arcee-ai/trinity-large-preview' | 'arcee-ai/trinity-large-thinking' | 'arcee-ai/trinity-mini' | 'bytedance/seed-1.6' | 'bytedance/seed-1.8' | 'cohere/command-a' | 'deepseek/deepseek-r1' | 'deepseek/deepseek-v3' | 'deepseek/deepseek-v3.1' | 'deepseek/deepseek-v3.1-terminus' | 'deepseek/deepseek-v3.2' | 'deepseek/deepseek-v3.2-thinking' | 'deepseek/deepseek-v4-flash' | 'deepseek/deepseek-v4-pro' | 'google/gemini-2.5-flash' | 'google/gemini-2.5-flash-image' | 'google/gemini-2.5-flash-lite' | 'google/gemini-2.5-pro' | 'google/gemini-3-flash' | 'google/gemini-3-pro-image' | 'google/gemini-3-pro-preview' | 'google/gemini-3.1-flash-image' | 'google/gemini-3.1-flash-image-preview' | 'google/gemini-3.1-flash-lite' | 'google/gemini-3.1-flash-lite-image' | 'google/gemini-3.1-flash-lite-preview' | 'google/gemini-3.1-pro-preview' | 'google/gemini-3.5-flash' | 'google/gemini-omni-flash-preview' | 'google/gemma-4-26b-a4b-it' | 'google/gemma-4-31b-it' | 'inception/mercury-2' | 'inception/mercury-coder-small' | 'interfaze/interfaze-beta' | 'kwaipilot/kat-coder-air-v2.5' | 'kwaipilot/kat-coder-pro-v1' | 'kwaipilot/kat-coder-pro-v2' | 'kwaipilot/kat-coder-pro-v2.5' | 'meituan/longcat-flash-chat' | 'meituan/longcat-flash-thinking-2601' | 'meta/llama-3.1-70b' | 'meta/llama-3.1-8b' | 'meta/llama-3.2-11b' | 'meta/llama-3.2-1b' | 'meta/llama-3.2-3b' | 'meta/llama-3.2-90b' | 'meta/llama-3.3-70b' | 'meta/llama-4-maverick' | 'meta/llama-4-scout' | 'meta/muse-spark-1.1' | 'minimax/minimax-m2' | 'minimax/minimax-m2.1' | 'minimax/minimax-m2.1-lightning' | 'minimax/minimax-m2.5' | 'minimax/minimax-m2.5-highspeed' | 'minimax/minimax-m2.7' | 'minimax/minimax-m2.7-highspeed' | 'minimax/minimax-m3' | 'mistral/codestral' | 'mistral/devstral-2' | 'mistral/devstral-small' | 'mistral/devstral-small-2' | 'mistral/magistral-medium' | 'mistral/magistral-small' | 'mistral/ministral-14b' | 'mistral/ministral-3b' | 'mistral/ministral-8b' | 'mistral/mistral-large-3' | 'mistral/mistral-medium' | 'mistral/mistral-medium-3.5' | 'mistral/mistral-nemo' | 'mistral/mistral-small' | 'mistral/pixtral-12b' | 'mistral/pixtral-large' | 'moonshotai/kimi-k2' | 'moonshotai/kimi-k2-thinking' | 'moonshotai/kimi-k2.5' | 'moonshotai/kimi-k2.6' | 'moonshotai/kimi-k2.7-code' | 'moonshotai/kimi-k2.7-code-highspeed' | 'morph/morph-v3-fast' | 'morph/morph-v3-large' | 'nvidia/nemotron-3-nano-30b-a3b' | 'nvidia/nemotron-3-super-120b-a12b' | 'nvidia/nemotron-3-ultra-550b-a55b' | 'nvidia/nemotron-nano-12b-v2-vl' | 'nvidia/nemotron-nano-9b-v2' | 'openai/gpt-3.5-turbo' | 'openai/gpt-3.5-turbo-instruct' | 'openai/gpt-4-turbo' | 'openai/gpt-4.1' | 'openai/gpt-4.1-mini' | 'openai/gpt-4.1-nano' | 'openai/gpt-4o' | 'openai/gpt-4o-mini' | 'openai/gpt-4o-mini-search-preview' | 'openai/gpt-5' | 'openai/gpt-5-chat' | 'openai/gpt-5-codex' | 'openai/gpt-5-mini' | 'openai/gpt-5-nano' | 'openai/gpt-5-pro' | 'openai/gpt-5.1-codex' | 'openai/gpt-5.1-codex-max' | 'openai/gpt-5.1-codex-mini' | 'openai/gpt-5.1-instant' | 'openai/gpt-5.1-thinking' | 'openai/gpt-5.2' | 'openai/gpt-5.2-chat' | 'openai/gpt-5.2-codex' | 'openai/gpt-5.2-pro' | 'openai/gpt-5.3-chat' | 'openai/gpt-5.3-codex' | 'openai/gpt-5.4' | 'openai/gpt-5.4-mini' | 'openai/gpt-5.4-nano' | 'openai/gpt-5.4-pro' | 'openai/gpt-5.5' | 'openai/gpt-5.5-pro' | 'openai/gpt-5.6-luna' | 'openai/gpt-5.6-sol' | 'openai/gpt-5.6-terra' | 'openai/gpt-oss-120b' | 'openai/gpt-oss-20b' | 'openai/gpt-oss-safeguard-20b' | 'openai/o1' | 'openai/o3' | 'openai/o3-deep-research' | 'openai/o3-mini' | 'openai/o3-pro' | 'openai/o4-mini' | 'perplexity/sonar' | 'perplexity/sonar-pro' | 'perplexity/sonar-reasoning-pro' | 'sakana/fugu-ultra' | 'stepfun/step-3.5-flash' | 'stepfun/step-3.7-flash' | 'xai/grok-4.1-fast-non-reasoning' | 'xai/grok-4.1-fast-reasoning' | 'xai/grok-4.20-multi-agent' | 'xai/grok-4.20-multi-agent-beta' | 'xai/grok-4.20-non-reasoning' | 'xai/grok-4.20-non-reasoning-beta' | 'xai/grok-4.20-reasoning' | 'xai/grok-4.20-reasoning-beta' | 'xai/grok-4.3' | 'xai/grok-4.5' | 'xai/grok-build-0.1' | 'xiaomi/mimo-v2-flash' | 'xiaomi/mimo-v2-pro' | 'xiaomi/mimo-v2.5' | 'xiaomi/mimo-v2.5-pro' | 'zai/glm-4.5' | 'zai/glm-4.5-air' | 'zai/glm-4.5v' | 'zai/glm-4.6' | 'zai/glm-4.6v' | 'zai/glm-4.6v-flash' | 'zai/glm-4.7' | 'zai/glm-4.7-flash' | 'zai/glm-4.7-flashx' | 'zai/glm-5' | 'zai/glm-5-turbo' | 'zai/glm-5.1' | 'zai/glm-5.2' | 'zai/glm-5.2-fast' | 'zai/glm-5v-turbo' | (string & {});

/**
* Shared WebSocket subprotocol contract for AI Gateway realtime auth.
* Shared WebSocket subprotocol contract for AI Gateway realtime and streaming
* transcription auth.
*

@@ -18,4 +19,5 @@ * The browser `WebSocket` API cannot set request headers, so the Gateway auth

* the Gateway server can't drift: the client encodes values with
* `getGatewayRealtimeProtocols`, and the Gateway server decodes them with
* `getGatewayRealtimeAuthToken` / `getGatewayRealtimeTeamIdOrSlug`.
* `getGatewayRealtimeProtocols` / `getGatewayTranscriptionProtocols`, and the
* Gateway server decodes them with `getGatewayRealtimeAuthToken` /
* `getGatewayRealtimeTeamIdOrSlug`.
*

@@ -29,7 +31,12 @@ * WebSocket subprotocol values must fit the RFC token grammar. The auth token is

/**
* Marker subprotocol offered on every handshake so the Gateway can echo a
* negotiated subprotocol on the 101 response (some clients require the server to
* select one of the offered subprotocols).
* Marker subprotocol offered on every realtime handshake so the Gateway can
* echo a negotiated subprotocol on the 101 response (some clients require the
* server to select one of the offered subprotocols).
*/
declare const GATEWAY_REALTIME_SUBPROTOCOL = "ai-gateway-realtime.v1";
/**
* Marker subprotocol for streaming transcription handshakes (same negotiation
* purpose as `GATEWAY_REALTIME_SUBPROTOCOL`).
*/
declare const GATEWAY_TRANSCRIPTION_SUBPROTOCOL = "ai-gateway-transcription.v1";
/** Subprotocol prefix that carries the Gateway auth (bearer) token. */

@@ -47,2 +54,9 @@ declare const GATEWAY_AUTH_SUBPROTOCOL_PREFIX = "ai-gateway-auth.";

/**
* Client-side: `getGatewayRealtimeProtocols`, but with the streaming
* transcription marker subprotocol.
*/
declare function getGatewayTranscriptionProtocols(token: string, options?: {
teamIdOrSlug?: string;
}): string[];
/**
* Server-side: extract the Gateway auth (bearer) token from a

@@ -70,3 +84,3 @@ * `Sec-WebSocket-Protocol` header value, or `undefined` when it is absent or

type GatewayTranscriptionModelId = 'openai/gpt-4o-mini-transcribe' | 'openai/gpt-4o-transcribe' | 'openai/whisper-1' | 'xai/grok-stt' | (string & {});
type GatewayTranscriptionModelId = 'openai/gpt-4o-mini-transcribe' | 'openai/gpt-4o-transcribe' | 'openai/gpt-realtime-whisper' | 'openai/whisper-1' | 'xai/grok-stt' | (string & {});

@@ -772,2 +786,9 @@ type GatewayVideoModelId = 'alibaba/wan-v2.5-t2v-preview' | 'alibaba/wan-v2.6-i2v' | 'alibaba/wan-v2.6-i2v-flash' | 'alibaba/wan-v2.6-r2v' | 'alibaba/wan-v2.6-r2v-flash' | 'alibaba/wan-v2.6-t2v' | 'alibaba/wan-v2.7-r2v' | 'alibaba/wan-v2.7-t2v' | 'bytedance/seedance-2.0' | 'bytedance/seedance-2.0-fast' | 'bytedance/seedance-v1.0-pro' | 'bytedance/seedance-v1.0-pro-fast' | 'bytedance/seedance-v1.5-pro' | 'google/veo-3.0-fast-generate-001' | 'google/veo-3.0-generate-001' | 'google/veo-3.1-fast-generate-001' | 'google/veo-3.1-generate-001' | 'klingai/kling-v2.5-turbo-i2v' | 'klingai/kling-v2.5-turbo-t2v' | 'klingai/kling-v2.6-i2v' | 'klingai/kling-v2.6-motion-control' | 'klingai/kling-v2.6-t2v' | 'klingai/kling-v3.0-i2v' | 'klingai/kling-v3.0-motion-control' | 'klingai/kling-v3.0-t2v' | 'xai/grok-imagine-video' | 'xai/grok-imagine-video-1.5' | 'xai/grok-imagine-video-1.5-preview' | (string & {});

/**
* Custom WebSocket implementation used for streaming transcription. This is
* useful for testing or for runtimes without a global WebSocket. A
* header-capable implementation is not required — Gateway WebSocket auth is
* carried in the subprotocols.
*/
webSocket?: WebSocketConstructor;
/**
* How frequently to refresh the metadata cache in milliseconds.

@@ -1022,2 +1043,2 @@ */

export { GATEWAY_AUTH_SUBPROTOCOL_PREFIX, GATEWAY_REALTIME_SUBPROTOCOL, GATEWAY_TEAM_SUBPROTOCOL_PREFIX, GatewayAuthenticationError, type GatewayCreditsResponse, GatewayError, type GatewayErrorResponse, GatewayFailedDependencyError, GatewayForbiddenError, type GatewayGenerationInfo, type GatewayGenerationInfoParams, GatewayInternalServerError, GatewayInvalidRequestError, type GatewayLanguageModelEntry, type GatewayProviderOptions as GatewayLanguageModelOptions, type GatewayLanguageModelSpecification, type GatewayLanguageModelEntry as GatewayModelEntry, type GatewayModelId, GatewayModelNotFoundError, type GatewayProvider, type GatewayProviderOptions, type GatewayProviderSettings, GatewayRateLimitError, type GatewayRealtimeModelId, type GatewayRerankingModelId, GatewayResponseError, type GatewaySpeechModelId, type GatewaySpendReportParams, type GatewaySpendReportResponse, type GatewaySpendReportRow, type GatewayTranscriptionModelId, type GatewayVideoModelId, VERSION, createGateway, createGateway as createGatewayProvider, gateway, getGatewayRealtimeAuthToken, getGatewayRealtimeProtocols, getGatewayRealtimeTeamIdOrSlug };
export { GATEWAY_AUTH_SUBPROTOCOL_PREFIX, GATEWAY_REALTIME_SUBPROTOCOL, GATEWAY_TEAM_SUBPROTOCOL_PREFIX, GATEWAY_TRANSCRIPTION_SUBPROTOCOL, GatewayAuthenticationError, type GatewayCreditsResponse, GatewayError, type GatewayErrorResponse, GatewayFailedDependencyError, GatewayForbiddenError, type GatewayGenerationInfo, type GatewayGenerationInfoParams, GatewayInternalServerError, GatewayInvalidRequestError, type GatewayLanguageModelEntry, type GatewayProviderOptions as GatewayLanguageModelOptions, type GatewayLanguageModelSpecification, type GatewayLanguageModelEntry as GatewayModelEntry, type GatewayModelId, GatewayModelNotFoundError, type GatewayProvider, type GatewayProviderOptions, type GatewayProviderSettings, GatewayRateLimitError, type GatewayRealtimeModelId, type GatewayRerankingModelId, GatewayResponseError, type GatewaySpeechModelId, type GatewaySpendReportParams, type GatewaySpendReportResponse, type GatewaySpendReportRow, type GatewayTranscriptionModelId, type GatewayVideoModelId, VERSION, createGateway, createGateway as createGatewayProvider, gateway, getGatewayRealtimeAuthToken, getGatewayRealtimeProtocols, getGatewayRealtimeTeamIdOrSlug, getGatewayTranscriptionProtocols };
{
"name": "@ai-sdk/gateway",
"private": false,
"version": "4.0.17",
"version": "4.0.19",
"type": "module",

@@ -35,3 +35,3 @@ "license": "Apache-2.0",

"@ai-sdk/provider": "4.0.3",
"@ai-sdk/provider-utils": "5.0.7"
"@ai-sdk/provider-utils": "5.0.9"
},

@@ -38,0 +38,0 @@ "devDependencies": {

@@ -1,2 +0,6 @@

import type { FetchFunction, Resolvable } from '@ai-sdk/provider-utils';
import type {
FetchFunction,
Resolvable,
WebSocketConstructor,
} from '@ai-sdk/provider-utils';

@@ -7,2 +11,3 @@ export type GatewayConfig = {

fetch?: FetchFunction;
webSocket?: WebSocketConstructor;
};

@@ -37,2 +37,3 @@ import {

url: `${this.config.baseURL}/config`,
validateUrl: false,
headers: this.config.headers

@@ -63,2 +64,3 @@ ? await resolve(this.config.headers)

url: `${baseUrl.origin}/v1/credits`,
validateUrl: false,
headers: this.config.headers

@@ -65,0 +67,0 @@ ? await resolve(this.config.headers)

@@ -68,2 +68,3 @@ import {

url: `${baseUrl.origin}/v1/generation?id=${encodeURIComponent(params.id)}`,
validateUrl: false,
headers: this.config.headers

@@ -70,0 +71,0 @@ ? await resolve(this.config.headers)

@@ -9,2 +9,3 @@ import {

type FetchFunction,
type WebSocketConstructor,
} from '@ai-sdk/provider-utils';

@@ -213,2 +214,10 @@ import { z } from 'zod/v4';

/**
* Custom WebSocket implementation used for streaming transcription. This is
* useful for testing or for runtimes without a global WebSocket. A
* header-capable implementation is not required — Gateway WebSocket auth is
* carried in the subprotocols.
*/
webSocket?: WebSocketConstructor;
/**
* How frequently to refresh the metadata cache in milliseconds.

@@ -521,2 +530,3 @@ */

o11yHeaders: createO11yHeaders(),
webSocket: options.webSocket,
});

@@ -523,0 +533,0 @@ };

/**
* Shared WebSocket subprotocol contract for AI Gateway realtime auth.
* Shared WebSocket subprotocol contract for AI Gateway realtime and streaming
* transcription auth.
*

@@ -11,4 +12,5 @@ * The browser `WebSocket` API cannot set request headers, so the Gateway auth

* the Gateway server can't drift: the client encodes values with
* `getGatewayRealtimeProtocols`, and the Gateway server decodes them with
* `getGatewayRealtimeAuthToken` / `getGatewayRealtimeTeamIdOrSlug`.
* `getGatewayRealtimeProtocols` / `getGatewayTranscriptionProtocols`, and the
* Gateway server decodes them with `getGatewayRealtimeAuthToken` /
* `getGatewayRealtimeTeamIdOrSlug`.
*

@@ -23,8 +25,14 @@ * WebSocket subprotocol values must fit the RFC token grammar. The auth token is

/**
* Marker subprotocol offered on every handshake so the Gateway can echo a
* negotiated subprotocol on the 101 response (some clients require the server to
* select one of the offered subprotocols).
* Marker subprotocol offered on every realtime handshake so the Gateway can
* echo a negotiated subprotocol on the 101 response (some clients require the
* server to select one of the offered subprotocols).
*/
export const GATEWAY_REALTIME_SUBPROTOCOL = 'ai-gateway-realtime.v1';
/**
* Marker subprotocol for streaming transcription handshakes (same negotiation
* purpose as `GATEWAY_REALTIME_SUBPROTOCOL`).
*/
export const GATEWAY_TRANSCRIPTION_SUBPROTOCOL = 'ai-gateway-transcription.v1';
/** Subprotocol prefix that carries the Gateway auth (bearer) token. */

@@ -44,7 +52,27 @@ export const GATEWAY_AUTH_SUBPROTOCOL_PREFIX = 'ai-gateway-auth.';

): string[] {
const protocols = [
GATEWAY_REALTIME_SUBPROTOCOL,
`${GATEWAY_AUTH_SUBPROTOCOL_PREFIX}${token}`,
];
return buildGatewayProtocols(GATEWAY_REALTIME_SUBPROTOCOL, token, options);
}
/**
* Client-side: `getGatewayRealtimeProtocols`, but with the streaming
* transcription marker subprotocol.
*/
export function getGatewayTranscriptionProtocols(
token: string,
options?: { teamIdOrSlug?: string },
): string[] {
return buildGatewayProtocols(
GATEWAY_TRANSCRIPTION_SUBPROTOCOL,
token,
options,
);
}
function buildGatewayProtocols(
marker: string,
token: string,
options?: { teamIdOrSlug?: string },
): string[] {
const protocols = [marker, `${GATEWAY_AUTH_SUBPROTOCOL_PREFIX}${token}`];
if (options?.teamIdOrSlug) {

@@ -51,0 +79,0 @@ protocols.push(

@@ -109,2 +109,3 @@ import {

url: `${baseUrl.origin}/v1/report?${searchParams.toString()}`,
validateUrl: false,
headers: this.config.headers

@@ -111,0 +112,0 @@ ? await resolve(this.config.headers)

export type GatewayTranscriptionModelId =
| 'openai/gpt-4o-mini-transcribe'
| 'openai/gpt-4o-transcribe'
| 'openai/gpt-realtime-whisper'
| 'openai/whisper-1'
| 'xai/grok-stt'
| (string & {});

@@ -1,19 +0,39 @@

import type {
SharedV4ProviderMetadata,
SharedV4Warning,
TranscriptionModelV4,
import {
getErrorMessage,
type Experimental_TranscriptionModelV4StreamOptions as TranscriptionModelV4StreamOptions,
type Experimental_TranscriptionModelV4StreamPart as TranscriptionModelV4StreamPart,
type Experimental_TranscriptionModelV4StreamResult as TranscriptionModelV4StreamResult,
type SharedV4ProviderMetadata,
type SharedV4Warning,
type TranscriptionModelV4,
} from '@ai-sdk/provider';
import {
combineHeaders,
convertBase64ToUint8Array,
convertUint8ArrayToBase64,
createJsonErrorResponseHandler,
createJsonResponseHandler,
connectToWebSocket,
normalizeHeaders,
experimental_parseTranscriptionStreamPart as parseTranscriptionStreamPart,
postJsonToApi,
resolve,
EXPERIMENTAL_TRANSCRIPTION_STREAM_AUDIO_DONE_FRAME_TYPE as TRANSCRIPTION_STREAM_AUDIO_DONE_FRAME_TYPE,
waitForWebSocketBufferDrain,
EXPERIMENTAL_TRANSCRIPTION_STREAM_START_FRAME_TYPE as TRANSCRIPTION_STREAM_START_FRAME_TYPE,
type Experimental_TranscriptionStreamStartFrame,
type Resolvable,
type WebSocketConnection,
type WebSocketConstructor,
type WebSocketLike,
} from '@ai-sdk/provider-utils';
import { z } from 'zod/v4';
import { asGatewayError } from './errors';
import { asGatewayError, createGatewayErrorFromResponse } from './errors';
import { parseAuthMethod } from './errors/parse-auth-method';
import type { GatewayConfig } from './gateway-config';
import { VERCEL_AI_GATEWAY_TEAM_HEADER } from './gateway-headers';
import {
GATEWAY_TRANSCRIPTION_SUBPROTOCOL,
getGatewayTranscriptionProtocols,
} from './gateway-realtime-auth';

@@ -28,2 +48,5 @@ export class GatewayTranscriptionModel implements TranscriptionModelV4 {

o11yHeaders: Resolvable<Record<string, string>>;
_internal?: {
currentDate?: () => Date;
};
},

@@ -103,2 +126,44 @@ ) {}

async doStream(
options: TranscriptionModelV4StreamOptions,
): Promise<TranscriptionModelV4StreamResult> {
const currentDate = this.config._internal?.currentDate?.() ?? new Date();
const headers = combineHeaders(
await resolve(this.config.headers ?? {}),
options.headers ?? {},
this.getModelConfigHeaders(),
await resolve(this.config.o11yHeaders),
);
const authMethod = await parseAuthMethod(headers);
// First frame after the WebSocket opens, per the transcription-stream
// envelope. Optional keys are omitted so the frame stays minimal.
const startFrame: Experimental_TranscriptionStreamStartFrame = {
type: TRANSCRIPTION_STREAM_START_FRAME_TYPE,
inputAudioFormat: options.inputAudioFormat,
...(options.providerOptions != null && {
providerOptions: options.providerOptions,
}),
...(options.includeRawChunks != null && {
includeRawChunks: options.includeRawChunks,
}),
};
return {
stream: createGatewayTranscriptionStream({
webSocket: this.config.webSocket,
url: toGatewayTranscriptionUrl(this.config.baseURL, this.modelId),
protocols: getProtocolsFromHeaders(headers),
headers,
startFrame,
audio: options.audio,
abortSignal: options.abortSignal,
authMethod,
}),
request: { body: startFrame },
response: { timestamp: currentDate, modelId: this.modelId },
};
}
private getUrl() {

@@ -116,2 +181,218 @@ return `${this.config.baseURL}/transcription-model`;

/**
* Gateway streaming transcription WebSocket URL: HTTP(S) base upgraded to
* WS(S), model id in `?ai-model-id=` (browser `WebSocket` cannot set headers;
* slash-safe for qualified ids like `openai/gpt-realtime-whisper`).
*/
function toGatewayTranscriptionUrl(baseURL: string, modelId: string): string {
const url = new URL(`${baseURL.replace(/^http/, 'ws')}/transcription-model`);
url.searchParams.set('ai-model-id', modelId);
return url.toString();
}
/**
* Auth-carrying subprotocols from the resolved request headers (bearer token
* + optional team scope). Native `WebSocket` cannot send headers, so auth
* rides the `Sec-WebSocket-Protocol` handshake. Header lookups are
* case-insensitive because `combineHeaders` does not normalize casing and
* callers can pass arbitrary casing via the `headers` option.
*/
function getProtocolsFromHeaders(
headers: Record<string, string | undefined>,
): string[] {
const normalizedHeaders = normalizeHeaders(headers);
const authorization = normalizedHeaders.authorization;
const token = authorization?.startsWith('Bearer ')
? authorization.slice('Bearer '.length)
: undefined;
return token == null
? [GATEWAY_TRANSCRIPTION_SUBPROTOCOL]
: getGatewayTranscriptionProtocols(token, {
teamIdOrSlug: normalizedHeaders[VERCEL_AI_GATEWAY_TEAM_HEADER],
});
}
/** Audio frames stay under server frame-size limits (envelope rule 7). */
const MAX_AUDIO_FRAME_BYTES = 64 * 1024;
function createGatewayTranscriptionStream({
webSocket,
url,
protocols,
headers,
startFrame,
audio,
abortSignal,
authMethod,
}: {
webSocket: WebSocketConstructor | undefined;
url: string;
protocols: string[];
headers: Record<string, string | undefined>;
startFrame: Experimental_TranscriptionStreamStartFrame;
audio: ReadableStream<Uint8Array | string>;
abortSignal: AbortSignal | undefined;
authMethod: 'api-key' | 'oidc' | undefined;
}): ReadableStream<TranscriptionModelV4StreamPart> {
let finished = false;
let cleanup: (closeCode?: number) => void = () => {};
return new ReadableStream<TranscriptionModelV4StreamPart>({
start: controller => {
let audioReader:
| ReadableStreamDefaultReader<Uint8Array | string>
| undefined;
let hasServerErrorPart = false;
let lastServerError: unknown;
let audioStopped = false;
let connection: WebSocketConnection | undefined;
cleanup = (closeCode?: number) => {
if (audioReader != null) {
void audioReader.cancel().catch(() => {});
} else {
// Pre-open failure or abort: `sendAudio` never took a reader, so
// cancel the caller's audio stream directly — otherwise an upstream
// producer piping into it hangs.
void audio.cancel().catch(() => {});
}
connection?.close(closeCode);
};
const stopAudio = () => {
audioStopped = true;
if (audioReader != null) {
void audioReader.cancel().catch(() => {});
audioReader = undefined;
} else {
void audio.cancel().catch(() => {});
}
};
const finishWithError = (error: unknown) => {
if (finished) return;
finished = true;
cleanup();
void errorControllerWithGatewayError(controller, error, authMethod);
};
const sendAudio = async (socket: WebSocketLike) => {
const reader = audio.getReader();
audioReader = reader;
try {
while (true) {
const { done, value } = await reader.read();
if (done || finished) break;
// Binary frames; base64 string chunks are decoded first. Caller
// chunks are split to stay under server frame-size limits
// (envelope rule 7), with backpressure between frames.
const bytes =
typeof value === 'string'
? convertBase64ToUint8Array(value)
: value;
for (
let offset = 0;
offset < bytes.length;
offset += MAX_AUDIO_FRAME_BYTES
) {
if (finished) break;
socket.send(
bytes.subarray(offset, offset + MAX_AUDIO_FRAME_BYTES),
);
await waitForWebSocketBufferDrain(socket);
}
}
} finally {
reader.releaseLock();
// unlocked again: cleanup must cancel `audio`, not the reader
if (audioReader === reader) {
audioReader = undefined;
}
}
if (!finished && !audioStopped) {
socket.send(
JSON.stringify({
type: TRANSCRIPTION_STREAM_AUDIO_DONE_FRAME_TYPE,
}),
);
}
};
connection = connectToWebSocket({
url,
protocols,
headers,
webSocket,
abortSignal,
onAbort: reason => {
if (finished) return;
finished = true;
cleanup();
controller.error(reason);
},
onProcessingError: finishWithError,
onOpen: socket => {
socket.send(JSON.stringify(startFrame));
void sendAudio(socket).catch(finishWithError);
},
// Server frames are envelope-serialized stream parts; the codec
// handles parsing, unknown-part skipping, and timestamp revival.
onMessageText: text => {
if (finished) return;
const part = parseTranscriptionStreamPart(text);
if (part == null) return;
if (part.type === 'finish') {
finished = true;
controller.enqueue(part);
controller.close();
cleanup(1000);
return;
}
if (part.type === 'error') {
// Remembered so the terminal close error (the server closes
// non-1000 after an error part) surfaces the server's message
// to promise-based consumers.
hasServerErrorPart = true;
lastServerError = part.error;
// envelope rule 5: error parts are terminal — stop sending audio
// while the server holds the connection open (e.g. for its final
// billing flush)
stopAudio();
}
controller.enqueue(part);
},
onSocketError: () => {
finishWithError(
new Error('Connection error on AI Gateway transcription stream'),
);
},
onClose: () => {
if (hasServerErrorPart) {
if (finished) return;
void createErrorFromServerErrorPart(
lastServerError,
authMethod,
).then(finishWithError);
return;
}
finishWithError(
new Error(
'AI Gateway transcription stream closed before a finish part was received',
),
);
},
});
},
cancel: () => {
if (finished) return;
finished = true;
cleanup();
},
});
}
const providerMetadataEntrySchema = z.object({}).catchall(z.unknown());

@@ -159,1 +440,69 @@

});
/**
* `asGatewayError` is async while the WebSocket event handlers are
* synchronous, hence this helper.
*/
async function errorControllerWithGatewayError(
controller: ReadableStreamDefaultController<TranscriptionModelV4StreamPart>,
error: unknown,
authMethod: 'api-key' | 'oidc' | undefined,
): Promise<void> {
controller.error(await asGatewayError(error, authMethod));
}
/**
* Extract a human-readable message from an `error` stream part's payload for
* the terminal close error.
*/
function getServerErrorMessage(error: unknown): string {
if (
error != null &&
typeof error === 'object' &&
'message' in error &&
typeof error.message === 'string'
) {
return error.message;
}
// JSON-stringifies object payloads (`String` would yield '[object Object]').
return getErrorMessage(error);
}
/** Canonical status codes for server error-part types (no HTTP status exists on the WebSocket). */
const SERVER_ERROR_STATUS_CODES: Record<string, number> = {
authentication_error: 401,
failed_dependency: 424,
forbidden: 403,
internal_server_error: 500,
invalid_request_error: 400,
model_not_found: 404,
rate_limit_exceeded: 429,
};
/**
* Maps a server error-part payload (`{ message, type }`) to the public
* Gateway error class for its type; unknown shapes keep the generic message.
*/
async function createErrorFromServerErrorPart(
error: unknown,
authMethod: 'api-key' | 'oidc' | undefined,
): Promise<unknown> {
if (
typeof error === 'object' &&
error != null &&
'message' in error &&
typeof error.message === 'string' &&
'type' in error &&
typeof error.type === 'string' &&
error.type in SERVER_ERROR_STATUS_CODES
) {
return createGatewayErrorFromResponse({
response: { error: { message: error.message, type: error.type } },
statusCode: SERVER_ERROR_STATUS_CODES[error.type],
authMethod,
});
}
return new Error(
`AI Gateway transcription stream failed: ${getServerErrorMessage(error)}`,
);
}

@@ -6,5 +6,7 @@ export type { GatewayModelId } from './gateway-language-model-settings';

GATEWAY_TEAM_SUBPROTOCOL_PREFIX,
GATEWAY_TRANSCRIPTION_SUBPROTOCOL,
getGatewayRealtimeAuthToken,
getGatewayRealtimeProtocols,
getGatewayRealtimeTeamIdOrSlug,
getGatewayTranscriptionProtocols,
} from './gateway-realtime-auth';

@@ -11,0 +13,0 @@ export type { GatewayRealtimeModelId } from './gateway-realtime-model-settings';

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display