@builder.io/ai-utils 0.81.3 → 0.83.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.
@@ -0,0 +1,158 @@
1
+ import { z } from "zod";
2
+ export const BUILDER_REALTIME_MODEL = "gpt-realtime-2.1";
3
+ export const BUILDER_REALTIME_MAX_SDP_LENGTH = 256000;
4
+ export const BUILDER_REALTIME_MAX_SESSION_BYTES = 64000;
5
+ const utf8ByteLength = (value) => new TextEncoder().encode(value).length;
6
+ const boundedRecord = (maxBytes, message) => z.record(z.string(), z.unknown()).refine((value) => {
7
+ try {
8
+ return utf8ByteLength(JSON.stringify(value)) <= maxBytes;
9
+ }
10
+ catch (_a) {
11
+ return false;
12
+ }
13
+ }, { message });
14
+ const RealtimeAudioFormatSchema = z.union([
15
+ z.string().min(1).max(128),
16
+ boundedRecord(2048, "Audio format configuration is too large"),
17
+ ]);
18
+ const RealtimeInputAudioSchema = z.object({
19
+ format: RealtimeAudioFormatSchema.optional(),
20
+ noise_reduction: z
21
+ .object({
22
+ type: z.enum(["near_field", "far_field"]),
23
+ })
24
+ .nullable()
25
+ .optional(),
26
+ transcription: z
27
+ .object({
28
+ model: z.literal("gpt-4o-mini-transcribe"),
29
+ language: z.string().min(1).max(32).optional(),
30
+ prompt: z.string().max(4096).optional(),
31
+ })
32
+ .nullable()
33
+ .optional(),
34
+ turn_detection: z
35
+ .object({
36
+ type: z.enum(["server_vad", "semantic_vad"]),
37
+ threshold: z.number().min(0).max(1).optional(),
38
+ prefix_padding_ms: z.number().int().min(0).max(10000).optional(),
39
+ silence_duration_ms: z.number().int().min(0).max(60000).optional(),
40
+ idle_timeout_ms: z.number().int().min(0).max(3600000).optional(),
41
+ eagerness: z.enum(["low", "medium", "high", "auto"]).optional(),
42
+ create_response: z.boolean().optional(),
43
+ interrupt_response: z.boolean().optional(),
44
+ })
45
+ .nullable()
46
+ .optional(),
47
+ });
48
+ const RealtimeOutputAudioSchema = z.object({
49
+ format: RealtimeAudioFormatSchema.optional(),
50
+ voice: z
51
+ .union([
52
+ z.string().min(1).max(128),
53
+ z.object({ id: z.string().min(1).max(256) }),
54
+ ])
55
+ .optional(),
56
+ speed: z.number().positive().max(4).optional(),
57
+ });
58
+ export const OpenAIRealtimeFunctionToolSchema = z.object({
59
+ type: z.literal("function"),
60
+ name: z
61
+ .string()
62
+ .min(1)
63
+ .max(64)
64
+ .regex(/^[a-zA-Z0-9_-]+$/),
65
+ description: z.string().max(4096).optional(),
66
+ parameters: boundedRecord(32000, "Realtime tool parameter schema is too large").optional(),
67
+ strict: z.boolean().optional(),
68
+ });
69
+ export const OpenAIRealtimeMcpToolSchema = z.object({
70
+ type: z.literal("mcp"),
71
+ server_label: z.string().min(1).max(128),
72
+ server_url: z.url().max(2048).optional(),
73
+ server_description: z.string().max(4096).optional(),
74
+ authorization: z.string().min(1).max(8192).optional(),
75
+ allowed_tools: z
76
+ .union([
77
+ z.array(z.string().min(1).max(128)).max(64),
78
+ boundedRecord(8000, "MCP allowed-tools configuration is too large"),
79
+ ])
80
+ .optional(),
81
+ require_approval: z
82
+ .union([
83
+ z.enum(["always", "never"]),
84
+ boundedRecord(8000, "MCP approval configuration is too large"),
85
+ ])
86
+ .optional(),
87
+ });
88
+ export const OpenAIRealtimeToolSchema = z.discriminatedUnion("type", [
89
+ OpenAIRealtimeFunctionToolSchema,
90
+ OpenAIRealtimeMcpToolSchema,
91
+ ]);
92
+ const OpenAIRealtimeToolChoiceSchema = z.union([
93
+ z.enum(["auto", "none", "required"]),
94
+ z.object({
95
+ type: z.literal("function"),
96
+ name: z.string().min(1).max(64),
97
+ }),
98
+ z.object({
99
+ type: z.literal("mcp"),
100
+ server_label: z.string().min(1).max(128),
101
+ name: z.string().min(1).max(128).optional(),
102
+ }),
103
+ ]);
104
+ /**
105
+ * The subset of OpenAI's Realtime session configuration accepted by Builder
106
+ * Connect. Zod objects intentionally strip unknown keys before this config is
107
+ * forwarded to OpenAI.
108
+ */
109
+ export const OpenAIRealtimeSessionConfigSchema = z
110
+ .object({
111
+ type: z.literal("realtime"),
112
+ model: z.literal(BUILDER_REALTIME_MODEL),
113
+ output_modalities: z.tuple([z.literal("audio")]),
114
+ instructions: z.string().max(32000).optional(),
115
+ audio: z
116
+ .object({
117
+ input: RealtimeInputAudioSchema.optional(),
118
+ output: RealtimeOutputAudioSchema.optional(),
119
+ })
120
+ .optional(),
121
+ tools: z.array(OpenAIRealtimeToolSchema).max(32).optional(),
122
+ tool_choice: OpenAIRealtimeToolChoiceSchema.optional(),
123
+ max_output_tokens: z
124
+ .union([z.literal("inf"), z.number().int().min(1).max(4096)])
125
+ .optional(),
126
+ prompt: z
127
+ .object({
128
+ id: z.string().min(1).max(256),
129
+ version: z.string().min(1).max(128).optional(),
130
+ variables: boundedRecord(16000, "Realtime prompt variables are too large").optional(),
131
+ })
132
+ .nullable()
133
+ .optional(),
134
+ tracing: z
135
+ .union([
136
+ z.literal("auto"),
137
+ boundedRecord(8000, "Realtime tracing configuration is too large"),
138
+ ])
139
+ .nullable()
140
+ .optional(),
141
+ truncation: z
142
+ .union([
143
+ z.enum(["auto", "disabled"]),
144
+ boundedRecord(2048, "Realtime truncation configuration is too large"),
145
+ ])
146
+ .optional(),
147
+ include: z.array(z.string().min(1).max(256)).max(16).optional(),
148
+ })
149
+ .refine((session) => utf8ByteLength(JSON.stringify(session)) <=
150
+ BUILDER_REALTIME_MAX_SESSION_BYTES, { message: "Realtime session configuration is too large" });
151
+ export const BuilderRealtimeSessionRequestSchema = z.object({
152
+ sdp: z
153
+ .string()
154
+ .min(1)
155
+ .max(BUILDER_REALTIME_MAX_SDP_LENGTH)
156
+ .refine((sdp) => sdp.trim().length > 0, { message: "SDP cannot be blank" }),
157
+ session: OpenAIRealtimeSessionConfigSchema,
158
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,122 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { BUILDER_REALTIME_MAX_SDP_LENGTH, BuilderRealtimeSessionRequestSchema, OpenAIRealtimeSessionConfigSchema, } from "./realtime.js";
3
+ const validSession = {
4
+ type: "realtime",
5
+ model: "gpt-realtime-2.1",
6
+ output_modalities: ["audio"],
7
+ instructions: "Help the user operate their Builder app.",
8
+ };
9
+ describe("BuilderRealtimeSessionRequestSchema", () => {
10
+ it("accepts a bounded audio session with tools", () => {
11
+ var _a;
12
+ const result = BuilderRealtimeSessionRequestSchema.parse({
13
+ sdp: "v=0\r\no=- 1 1 IN IP4 127.0.0.1",
14
+ session: {
15
+ ...validSession,
16
+ audio: {
17
+ input: {
18
+ turn_detection: {
19
+ type: "semantic_vad",
20
+ eagerness: "auto",
21
+ create_response: true,
22
+ interrupt_response: true,
23
+ },
24
+ },
25
+ output: { voice: "marin" },
26
+ },
27
+ tools: [
28
+ {
29
+ type: "function",
30
+ name: "navigate",
31
+ description: "Navigate within the app",
32
+ parameters: {
33
+ type: "object",
34
+ properties: { path: { type: "string" } },
35
+ required: ["path"],
36
+ },
37
+ },
38
+ ],
39
+ },
40
+ });
41
+ expect(result.session.model).toBe("gpt-realtime-2.1");
42
+ expect((_a = result.session.tools) === null || _a === void 0 ? void 0 : _a[0]).toMatchObject({
43
+ type: "function",
44
+ name: "navigate",
45
+ });
46
+ });
47
+ it("strips unknown request, session, and known nested config keys", () => {
48
+ const sdp = " v=0\r\n";
49
+ const result = BuilderRealtimeSessionRequestSchema.parse({
50
+ sdp,
51
+ ignoredRequestValue: "secret",
52
+ session: {
53
+ ...validSession,
54
+ ignoredSessionValue: "secret",
55
+ audio: {
56
+ output: {
57
+ voice: "marin",
58
+ ignoredAudioValue: "secret",
59
+ },
60
+ },
61
+ },
62
+ });
63
+ expect(result).toEqual({
64
+ sdp,
65
+ session: {
66
+ ...validSession,
67
+ audio: { output: { voice: "marin" } },
68
+ },
69
+ });
70
+ });
71
+ it.each([
72
+ [{ ...validSession, type: "transcription" }],
73
+ [{ ...validSession, model: "gpt-realtime" }],
74
+ [{ ...validSession, output_modalities: ["text"] }],
75
+ [{ ...validSession, output_modalities: ["audio", "text"] }],
76
+ [{ ...validSession, output_modalities: [] }],
77
+ ])("rejects unsupported sessions", (session) => {
78
+ expect(BuilderRealtimeSessionRequestSchema.safeParse({ sdp: "v=0", session })
79
+ .success).toBe(false);
80
+ });
81
+ it("rejects transcription models that do not match the metered rate", () => {
82
+ expect(OpenAIRealtimeSessionConfigSchema.safeParse({
83
+ ...validSession,
84
+ audio: {
85
+ input: { transcription: { model: "gpt-4o-transcribe" } },
86
+ },
87
+ }).success).toBe(false);
88
+ });
89
+ it("rejects empty and oversized SDP offers", () => {
90
+ expect(BuilderRealtimeSessionRequestSchema.safeParse({
91
+ sdp: " ",
92
+ session: validSession,
93
+ }).success).toBe(false);
94
+ expect(BuilderRealtimeSessionRequestSchema.safeParse({
95
+ sdp: "x".repeat(BUILDER_REALTIME_MAX_SDP_LENGTH + 1),
96
+ session: validSession,
97
+ }).success).toBe(false);
98
+ });
99
+ it("bounds instructions, tool count, and tool parameter schemas", () => {
100
+ expect(OpenAIRealtimeSessionConfigSchema.safeParse({
101
+ ...validSession,
102
+ instructions: "x".repeat(32001),
103
+ }).success).toBe(false);
104
+ expect(OpenAIRealtimeSessionConfigSchema.safeParse({
105
+ ...validSession,
106
+ tools: Array.from({ length: 33 }, (_, index) => ({
107
+ type: "function",
108
+ name: `tool_${index}`,
109
+ })),
110
+ }).success).toBe(false);
111
+ expect(OpenAIRealtimeSessionConfigSchema.safeParse({
112
+ ...validSession,
113
+ tools: [
114
+ {
115
+ type: "function",
116
+ name: "large_tool",
117
+ parameters: { description: "x".repeat(32001) },
118
+ },
119
+ ],
120
+ }).success).toBe(false);
121
+ });
122
+ });