உலகம் நின்றுவிடவில்லை.RSS
BIG CHANGE.

Markdown பதிப்பு

AI-translated from English; not yet reviewed by a fluent editor.

# படிக்க மட்டுமே அனுமதிக்கும் கருவிகளுடன் உள்ளூர் Realtime குரல் முகவரை உருவாக்குங்கள்

> OpenAI Realtime மற்றும் இரண்டு கற்பனையான படிக்க மட்டும் உதவும் கருவிகளால் உலாவியில் நேரலைக் குரல் உரையாடலை உருவாக்குங்கள். சேவையகம், உலாவி, கட்டுப்படுத்தியின் முழுக் குறியீடு அழைப்பு ID பொருத்தம், பாதுகாப்பான தொடர்ச்சி, இடைமறித்தல், ஒலிவாங்கி விடுவித்தலைக் காட்டுகிறது.

By BIG CHANGE Editorial

Published: 2026-10-19T23:39:53.236Z
Updated: 2026-10-19T23:39:53.236Z
Canonical: https://bigchange.ai/blog/openai-realtime-voice-read-only-tool-calls-guide

![Conceptual charcoal illustration of a person speaking into a tabletop microphone while wearing headphones, with a compact computer and two separate unmarked modules nearby.](https://bigchange.ai/api/media/file/realtime-voice-tool-calls-hero-v1.png)
AI-generated conceptual illustration by BIG CHANGE; no actual OpenAI interface, API session, device test or heard answer is depicted.

பேசும் பதிலுக்குப் பல தேடல்கள் தேவைப்பட்டால் உலாவிக் குரல் உரையாடல் சிக்கலாகிறது. மாதிரி ஒரு நிறைவடைந்த பதிலில் பல செயல்பாட்டு அழைப்புகளைத் தரலாம். ஒவ்வொரு முடிவும் அதன் அசல் `call_id` உடன் அனுப்பப்பட வேண்டும்; அனைத்து முடிவுகளையும் அனுப்பிய பிறகே புதிய பதிலைத் தொடங்க வேண்டும். இந்த வழிகாட்டி உருவாக்குநர்களுக்கு முழு localhost எடுத்துக்காட்டைத் தருகிறது: நேரலை ஒலிவாங்கி உள்ளீடு, WebRTC தொடரோட்ட இயக்கம், இரண்டு கற்பனையான படிக்க மட்டும் கருவிகள், மீண்டும் உரையாடல்கள், இடைமறிப்பில் துப்புரவு. இது எங்கள் [அடிப்படை உலாவிக் குரல் வழிகாட்டியை](https://bigchange.ai/blog/openai-realtime-browser-voice-webrtc-guide) விரிவாக்குகிறது; அது வெளிக்கருவிகள் இல்லாமல் பேசிய பதிலை உருவாக்குகிறது.

## பெரிய மாற்றம்

- **என்ன மாறியது:** OpenAI வழங்கும் [Realtime API](https://developers.openai.com/api/docs/guides/realtime-conversations) நேரலைக் குரல் அமர்வில் செயல்பாட்டு அழைப்புகளைத் தரலாம். இந்த எடுத்துக்காட்டு `call_id` மூலம் முடிந்த அழைப்புகளை உள்ளூர் முடிவுகளுடன் இணைத்து, பின்னர் மாதிரியைத் தொடரச் சொல்கிறது.
- **இது ஏன் முக்கியம்:** பல படிக்க மட்டும் தேடல்களைச் சார்ந்த பேச்சுப் பதிலை உருவாக்குநர் சோதிக்கலாம். ஒலிவாங்கிப் பதிவு, இயக்கம் உலாவியிலும் வழக்கமான API சாவி உள்ளூர் சேவையகத்திலும் இருக்கும்.
- **கவனிக்க வேண்டியவை:** உள்ளூர் சோதனைகள் செயற்கை நிகழ்வுகளால் வரிசை, துப்புரவைச் சோதிக்கின்றன. உங்கள் கணக்கு, உலாவி, சாதனங்கள், வலைப்பின்னல் கேட்கக்கூடிய சரியான பதில்களையும் எதிர்பார்த்த கட்டணத்தையும் தருகிறதா என்பதை உங்கள் சொந்தக் கட்டண அமர்வில்தான் உறுதிசெய்ய வேண்டும்.

எடுத்துக்காட்டு OpenAI வழக்கமான Realtime WebRTC முனையமான `/v1/realtime/calls` உடன் `gpt-realtime-2.1` பயன்படுத்துகிறது. தனியான GPT-Live அமர்வு நெறிமுறையைப் பயன்படுத்தவில்லை. கீழே உள்ள இரண்டு “நூலக” மதிப்புகள் கற்பனை மென்பொருள் சோதனைத் தரவு; உண்மையான இடத் தகவல் அல்ல. BIG CHANGE 2026 அக்டோபர் 20 அன்று தற்போதைய [WebRTC](https://developers.openai.com/api/docs/guides/voice-webrtc), [உரையாடல்](https://developers.openai.com/api/docs/guides/realtime-conversations), [கருவி](https://developers.openai.com/api/docs/guides/realtime-mcp), [நிகழ்வு](https://developers.openai.com/api/reference/resources/realtime/client-events) ஆவணங்களைச் சரிபார்த்தது. உள்ளூர் செயற்கைக் கட்டுப்படுத்தி, உலாவிச் சோதனைகளை இயக்கினோம்; கட்டண API அமர்வைத் திறக்கவோ உண்மையான ஒலிவாங்கி அல்லது ஒலியைச் சோதிக்கவோ இல்லை.

## 1. அணுகலைச் சரிபார்த்து ஐந்து கோப்புகளை உருவாக்குங்கள்

Node.js 22.6 அல்லது புதியது, ஒலிவாங்கி, ஒலிபெருக்கிகள் கொண்ட தற்போதைய உலாவி, மேலும் `gpt-realtime-2.1` அணுகலுள்ள OpenAI API திட்டம் வேண்டும். அதன் [மாதிரிப் பக்கம்](https://developers.openai.com/api/docs/models/gpt-realtime-2.1) Free நிலை ஆதரிக்கப்படவில்லை என்கிறது. முதலில் API கட்டணம், திட்டக் கோரிக்கை வீதம், செலவு வரம்புகளைப் பாருங்கள்; ChatGPT சந்தா API பயன்பாட்டைச் செலுத்தாது. வழக்கமான உள்ளூர் ரகசிய மேலாண்மை முறையில் `OPENAI_API_KEY` ஐ **சேவையகச் செயல்முறைச் சூழலில்** வையுங்கள். இந்தக் கோப்புகள், உலாவிக் குறியீடு அல்லது உரையாடல் எழுத்துப்பதிவில் சாவியை எழுதாதீர்கள். OpenAI க்கு அனுப்ப அனுமதிக்கப்பட்ட பேச்சை மட்டும் பயன்படுத்துங்கள்.

புதிய அடைவு உருவாக்கி கீழே உள்ள ஐந்து முழுத் தொகுதிகளையும் அவற்றுக்கு மேலுள்ள கோப்புப் பெயர்களில் சேமியுங்கள். `.mjs` நீட்சிகளை வைத்திருந்து அனைத்து கோப்புகளையும் ஒரே அடைவில் வையுங்கள். சேவையகம் `127.0.0.1:3000` இல் கேட்கிறது. உள்ளூர் பக்கத்தின் SDP offer ஐப் பெற்று சேவையகச் சாவியால் OpenAI க்கு அனுப்பி SDP answer ஐத் தருகிறது. உலாவிக்கு வழக்கமான API சாவி கிடைக்காது. உள்ளூர் Host, Origin சோதனைகள் செயல்விளக்கத்தை localhost க்குள் வைக்க உதவுகின்றன; பகிரப்பட்ட சேவைப் பயனர்களை அங்கீகரிப்பதில்லை.

**server.mjs**

```javascript
import express from "express";

const app = express();
// This demo accepts only the local page, including protection against DNS rebinding.
app.use((req, res, next) => {
  const hosts = ["localhost:3000", "127.0.0.1:3000"];
  if (!hosts.includes(req.get("host"))) return res.status(403).send("Local host required");
  if (req.method === "POST" && req.get("origin") !== `http://${req.get("host")}`) {
    return res.status(403).send("Same-origin local page required");
  }
  next();
});
app.use(express.text({ type: "application/sdp", limit: "64kb" }));

app.get("/", (_req, res) => res.sendFile("index.html", { root: process.cwd() }));
app.get("/controller.mjs", (_req, res) => res.sendFile("controller.mjs", { root: process.cwd() }));

app.post("/session", async (req, res) => {
  if (!process.env.OPENAI_API_KEY) return res.status(500).send("Server API key missing");
  if (typeof req.body !== "string" || !req.body.startsWith("v=")) {
    return res.status(400).send("Expected an SDP offer");
  }

  const form = new FormData();
  form.set("sdp", req.body);
  form.set("session", JSON.stringify({
    type: "realtime",
    model: "gpt-realtime-2.1",
    output_modalities: ["audio"],
    audio: { output: { voice: "marin" } },
    instructions: "Answer questions about the fictional demo library only using its read-only tools. State clearly that the data is synthetic software test data, not real venue information. For a lookup that may take noticeable time, give one short spoken progress preamble when useful, then call the tools. A preamble is never a result. Do not invent missing information. If a tool result is an error, say that lookup failed. Respond concisely in speech.",
    tools: [
      { type: "function", name: "get_demo_hours", description: "Read synthetic demo-library opening hours; no external account or network.", parameters: { type: "object", properties: {}, additionalProperties: false } },
      { type: "function", name: "get_demo_address", description: "Read synthetic demo-library address; no external account or network.", parameters: { type: "object", properties: {}, additionalProperties: false } }
    ],
    tool_choice: "auto"
  }));

  try {
    const upstream = await fetch("https://api.openai.com/v1/realtime/calls", {
      method: "POST",
      headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
      body: form,
      signal: AbortSignal.timeout(25000)
    });
    const answer = await upstream.text();
    if (!upstream.ok) {
      console.error("Realtime session rejected", upstream.status);
      return res.status(502).send(`OpenAI session request failed (${upstream.status})`);
    }
    res.type("application/sdp").send(answer);
  } catch (error) {
    console.error("Realtime session network failure", error.name);
    res.status(502).send("Could not reach the Realtime API");
  }
});

app.listen(3000, "127.0.0.1", () => console.log("Open http://localhost:3000"));
```

**index.html**

```html
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Local Realtime voice and read-only tool check</title>
<h1>Local voice and tool check</h1>
<p>The library data in this demo is synthetic software test data.</p>
<button id="start">Start voice</button>
<button id="stop" disabled>Stop and release microphone</button>
<p id="status" role="status">Idle</p>
<audio id="reply" autoplay controls></audio>
<pre id="events" aria-label="Event status"></pre>
<script type="module">
import { ToolController } from "/controller.mjs";

const start = document.querySelector("#start");
const stop = document.querySelector("#stop");
const status = document.querySelector("#status");
const events = document.querySelector("#events");
const reply = document.querySelector("#reply");
let current;

function note(line) {
  events.textContent = `${line}\n${events.textContent}`.slice(0, 3000);
}

// These values are synthetic test fixtures, never real business records.
async function runDemoTool(call, signal) {
  if (signal.aborted) throw Error("interrupted");
  let args;
  try { args = JSON.parse(call.arguments); } catch { return { ok: false, error: "Invalid arguments" }; }
  if (!args || Array.isArray(args) || typeof args !== "object" || Object.keys(args).length) {
    return { ok: false, error: "Expected an empty object" };
  }
  if (call.name === "get_demo_hours") return { ok: true, value: "Synthetic demo library hours: Monday-Friday 09:00-17:00." };
  if (call.name === "get_demo_address") return { ok: true, value: "Synthetic demo library address: 1 Example Lane. This is not a real venue." };
  return { ok: false, error: "Tool is not allowed" };
}

function closeSession(session = current, message = "Stopped") {
  if (!session || session.closed) return;
  session.closed = true;
  clearTimeout(session.setupTimer);
  clearTimeout(session.sessionTimer);
  session.controller?.close();
  session.abort.abort();
  session.mic?.getTracks().forEach(track => track.stop());
  if (session.pc) session.pc.onconnectionstatechange = null;
  session.dc?.close();
  session.pc?.close();
  reply.pause();
  if (current === session) {
    current = null;
    reply.srcObject = null;
    start.disabled = false;
    stop.disabled = true;
    status.textContent = message;
    note(message);
  }
}

start.onclick = async () => {
  const session = { abort: new AbortController(), closed: false };
  current = session;
  start.disabled = true;
  stop.disabled = false;
  status.textContent = "Requesting microphone";
  events.textContent = "";
  session.setupTimer = setTimeout(() => closeSession(session, "Setup timed out; microphone stopped"), 30000);
  try {
    const mic = await navigator.mediaDevices.getUserMedia({ audio: true });
    if (session.closed) { mic.getTracks().forEach(track => track.stop()); return; }
    session.mic = mic;
    const pc = session.pc = new RTCPeerConnection();
    pc.ontrack = event => {
      if (session.closed) return;
      reply.srcObject = event.streams[0];
      reply.play().catch(() => note("Playback blocked; press Play on the audio control"));
    };
    pc.onconnectionstatechange = () => {
      if (session.closed) return;
      note(`Peer: ${pc.connectionState}`);
      if (pc.connectionState === "connected") {
        clearTimeout(session.setupTimer);
        clearTimeout(session.sessionTimer);
        session.sessionTimer = setTimeout(() => closeSession(session, "Local session limit reached; microphone stopped"), 55 * 60000);
        status.textContent = "Connected; ask about the synthetic demo library";
      }
      if (["failed", "closed", "disconnected"].includes(pc.connectionState)) closeSession(session, "Connection ended; microphone stopped");
    };
    pc.addTrack(mic.getAudioTracks()[0], mic);
    const dc = session.dc = pc.createDataChannel("oai-events");
    const send = event => {
      if (session.closed || dc.readyState !== "open") throw Error("Event channel is not open");
      dc.send(JSON.stringify(event));
    };
    session.controller = new ToolController({
      send, runTool: runDemoTool,
      status: note,
      fatal: message => closeSession(session, `${message}; microphone stopped`)
    });
    dc.onmessage = message => {
      if (session.closed) return;
      let event;
      try { event = JSON.parse(message.data); } catch { closeSession(session, "Invalid event; microphone stopped"); return; }
      if (event.type === "session.created") note("Realtime session created");
      if (event.type === "output_audio_buffer.cleared") note("Server cleared unplayed speech after interruption");
      session.controller.handle(event).catch(() => closeSession(session, "Controller failed; microphone stopped"));
    };
    dc.onclose = () => { if (!session.closed) closeSession(session, "Event channel closed; microphone stopped"); };
    dc.onerror = () => closeSession(session, "Event channel failed; microphone stopped");
    const offer = await pc.createOffer();
    await pc.setLocalDescription(offer);
    if (session.closed) return;
    const response = await fetch("/session", {
      method: "POST", headers: { "Content-Type": "application/sdp" },
      body: offer.sdp, signal: session.abort.signal
    });
    if (!response.ok) throw Error(`Session request failed (${response.status})`);
    const answer = await response.text();
    if (session.closed) return;
    await pc.setRemoteDescription({ type: "answer", sdp: answer });
    if (!session.closed) status.textContent = "Signaling complete; waiting for connection";
  } catch (error) {
    if (!session.closed) closeSession(session, `Start failed: ${error.message}`);
  }
};
stop.onclick = () => closeSession();
window.addEventListener("pagehide", () => closeSession());
</script>
</html>
```

**controller.mjs**

```javascript
// The same controller runs in the browser and in the synthetic local checks.
export class ToolController {
  constructor({ send, runTool, status, fatal, setTimer = setTimeout, clearTimer = clearTimeout }) {
    Object.assign(this, { send, runTool, status, fatal, setTimer, clearTimer });
    this.epoch = 0;
    this.rounds = 0;
    this.counter = 0;
    this.responses = new Map();
    this.pending = null;
    this.closed = false;
  }

  abortPending() {
    if (!this.pending) return;
    this.pending.abort.abort();
    if (this.pending.timer) this.clearTimer(this.pending.timer);
    this.pending = null;
  }

  close() {
    this.closed = true;
    this.epoch++;
    this.abortPending();
    this.responses.clear();
  }

  fail(message) {
    if (this.closed) return;
    this.status(message);
    this.close();
    this.fatal(message);
  }

  interrupt() {
    if (this.closed) return;
    this.epoch++;
    this.rounds = 0;
    this.abortPending();
    this.status("Speech detected; interrupted result discarded");
  }

  async handle(event) {
    if (this.closed) return;
    if (event.type === "error") {
      this.fail(`Realtime error: ${event.error?.code || event.error?.message || "unknown"}`);
      return;
    }
    if (event.type === "input_audio_buffer.speech_started") { this.interrupt(); return; }
    if (event.type === "response.created") {
      if (event.response?.id) this.responses.set(event.response.id, this.epoch);
      return;
    }
    if (event.type === "conversation.item.done") {
      const p = this.pending;
      const id = event.item?.id;
      if (!p || p.epoch !== this.epoch || !p.acks.has(id)) return;
      p.acks.delete(id);
      if (p.acks.size === 0) {
        if (p.timer) this.clearTimer(p.timer);
        this.pending = null;
        if (!p.abort.signal.aborted && !this.closed && p.epoch === this.epoch) {
          this.status("All tool outputs acknowledged; resuming response");
          this.send({ type: "response.create" });
        }
      }
      return;
    }
    if (event.type !== "response.done") return;
    const response = event.response;
    const id = response?.id;
    const responseEpoch = this.responses.get(id);
    this.responses.delete(id);
    if (responseEpoch === undefined || responseEpoch !== this.epoch) return;
    this.status(`Response ${id}: ${response.status || "unknown"}`);
    if (response.status !== "completed") return; // Never execute partial/cancelled calls.
    const calls = (response.output || []).filter(item => item.type === "function_call");
    if (!calls.length) return;
    if (this.pending) { this.fail("Overlapping tool batches in one turn"); return; }
    if (calls.length > 8 || ++this.rounds > 3) { this.fail("Tool call or round bound reached"); return; }
    const ids = new Set();
    for (const call of calls) {
      if (call.status !== "completed" || !call.call_id || ids.has(call.call_id)) {
        this.fail("Incomplete or repeated function call ID"); return;
      }
      ids.add(call.call_id);
    }
    const p = { epoch: this.epoch, abort: new AbortController(), acks: new Set(), timer: null };
    this.pending = p;
    this.status(`Executing ${calls.length} read-only local call(s) from ${id}`);
    const outputs = await Promise.all(calls.map(async call => {
      try { return { call, result: await this.runTool(call, p.abort.signal) }; }
      catch { return { call, result: { ok: false, error: "Local lookup failed" } }; }
    }));
    if (this.closed || this.pending !== p || p.epoch !== this.epoch || p.abort.signal.aborted) return;
    try {
      for (const { call, result } of outputs) {
        const itemId = `tool_output_${++this.counter}`;
        p.acks.add(itemId);
        this.send({
          type: "conversation.item.create",
          event_id: `tool_event_${this.counter}`,
          item: { id: itemId, type: "function_call_output", call_id: call.call_id, output: JSON.stringify(result) }
        });
        this.status(`Sent result for ${call.call_id}`);
      }
      p.timer = this.setTimer(() => this.fail("Tool output acknowledgement timed out"), 8000);
    } catch { this.fail("Could not send tool outputs"); }
  }
}
```

**protocol-check.mjs**

```javascript
import assert from "node:assert/strict";
import { ToolController } from "./controller.mjs";

const deferred = () => { let resolve, reject; const promise = new Promise((a, b) => { resolve = a; reject = b; }); return { promise, resolve, reject }; };
const tick = () => new Promise(resolve => setImmediate(resolve));
const call = (id, name = "get_demo_hours") => ({ type: "function_call", status: "completed", call_id: id, name, arguments: "{}" });
const created = id => ({ type: "response.created", response: { id } });
const done = (id, status, output = []) => ({ type: "response.done", response: { id, status, output } });

function harness(runTool = async x => ({ ok: true, value: x.name })) {
  const sent = [], statuses = [], failures = [];
  const timers = new Map(); let nextTimer = 0;
  const controller = new ToolController({
    send: e => sent.push(e), runTool,
    status: s => statuses.push(s), fatal: e => failures.push(e),
    setTimer: callback => { const id = ++nextTimer; timers.set(id, callback); return id; },
    clearTimer: id => timers.delete(id)
  });
  return { controller, sent, statuses, failures, timers };
}

// Mixed audio/message output, two asynchronous calls resolved in reverse order.
{
  const a = deferred(), b = deferred();
  const h = harness(x => x.call_id === "call_A" ? a.promise : b.promise);
  await h.controller.handle(created("resp_1"));
  const work = h.controller.handle(done("resp_1", "completed", [
    { type: "message", status: "completed", content: [{ type: "output_audio", transcript: "Checking." }] },
    call("call_A"), call("call_B", "get_demo_address")
  ]));
  b.resolve({ ok: true, value: "address" }); await tick();
  assert.equal(h.sent.length, 0, "first finished lookup must not send or resume early");
  a.resolve({ ok: true, value: "hours" }); await work;
  assert.deepEqual(h.sent.map(x => x.item?.call_id), ["call_A", "call_B"]);
  assert.equal(h.sent.filter(x => x.type === "response.create").length, 0);
  await h.controller.handle({ type: "conversation.item.done", item: { id: h.sent[1].item.id } });
  assert.equal(h.sent.filter(x => x.type === "response.create").length, 0);
  await h.controller.handle({ type: "conversation.item.done", item: { id: h.sent[0].item.id } });
  assert.equal(h.sent.filter(x => x.type === "response.create").length, 1, "one resume after both item acknowledgements");
}

// One local lookup error does not erase the other result.
{
  const h = harness(async x => { if (x.call_id === "bad") throw Error("synthetic failure"); return { ok: true, value: "fixture" }; });
  await h.controller.handle(created("resp_2"));
  await h.controller.handle(done("resp_2", "completed", [call("bad"), call("good")]));
  assert.equal(JSON.parse(h.sent[0].item.output).ok, false);
  assert.equal(JSON.parse(h.sent[1].item.output).ok, true);
  for (const e of h.sent.slice()) await h.controller.handle({ type: "conversation.item.done", item: { id: e.item.id } });
  assert.equal(h.sent.filter(x => x.type === "response.create").length, 1);
}

// Partial/cancelled responses never dispatch a call.
for (const status of ["failed", "incomplete", "cancelled"]) {
  const h = harness();
  await h.controller.handle(created(`resp_${status}`));
  await h.controller.handle(done(`resp_${status}`, status, [call("ignored")]));
  assert.equal(h.sent.length, 0);
}

// Distinct rounds are bounded, and missing/duplicate IDs are rejected.
{
  const h = harness();
  for (let i = 1; i <= 3; i++) {
    await h.controller.handle(created(`round_${i}`));
    await h.controller.handle(done(`round_${i}`, "completed", [call(`call_${i}`)]));
    const last = h.sent.at(-1);
    await h.controller.handle({ type: "conversation.item.done", item: { id: last.item.id } });
  }
  await h.controller.handle(created("round_4"));
  await h.controller.handle(done("round_4", "completed", [call("too_many")]));
  assert.match(h.failures[0], /bound/);
  assert.equal(h.sent.filter(x => x.type === "response.create").length, 3);
}
{
  const h = harness();
  await h.controller.handle(created("dup"));
  await h.controller.handle(done("dup", "completed", [call("same"), call("same")]));
  assert.match(h.failures[0], /repeated/);
}

// User speech or Stop invalidates in-flight lookups and ignores their late results.
for (const mode of ["speech", "stop"]) {
  const d = deferred(), h = harness(() => d.promise);
  await h.controller.handle(created(`resp_${mode}`));
  const work = h.controller.handle(done(`resp_${mode}`, "completed", [call("late")]));
  if (mode === "speech") await h.controller.handle({ type: "input_audio_buffer.speech_started" });
  else h.controller.close();
  d.resolve({ ok: true, value: "late fixture" }); await work;
  assert.equal(h.sent.length, 0);
}

// A late response completion or acknowledgement from the interrupted turn cannot resume it.
{
  const h = harness();
  await h.controller.handle(created("old"));
  await h.controller.handle({ type: "input_audio_buffer.speech_started" });
  await h.controller.handle(done("old", "completed", [call("late_call")]));
  assert.equal(h.sent.length, 0);
  await h.controller.handle(created("new"));
  await h.controller.handle(done("new", "completed", [call("fresh_call")]));
  const output = h.sent[0];
  await h.controller.handle({ type: "input_audio_buffer.speech_started" });
  await h.controller.handle({ type: "conversation.item.done", item: { id: output.item.id } });
  assert.equal(h.sent.filter(x => x.type === "response.create").length, 0);
}

// Missing server acknowledgement closes instead of speculatively resuming.
{
  const h = harness();
  await h.controller.handle(created("no_ack"));
  await h.controller.handle(done("no_ack", "completed", [call("one")]));
  assert.equal(h.timers.size, 1);
  [...h.timers.values()][0]();
  assert.match(h.failures[0], /timed out/);
  assert.equal(h.sent.filter(x => x.type === "response.create").length, 0);
}

console.log("Synthetic controller checks passed: ordering, single resume, error isolation, bounds and interruption.");
```

**browser-check.mjs**

```javascript
import assert from "node:assert/strict";
import fs from "node:fs";
import vm from "node:vm";
import { ToolController } from "./controller.mjs";

const html = fs.readFileSync(new URL("./index.html", import.meta.url), "utf8");
const script = html.match(/<script type="module">([\s\S]*?)<\/script>/)?.[1]
  .replace('import { ToolController } from "/controller.mjs";', "");
assert.ok(script, "inline browser module found");

function fakePage(mediaPromise) {
  const nodes = Object.fromEntries(["#start", "#stop", "#status", "#events", "#reply"].map(k => [k, {
    disabled: k === "#stop", textContent: "", srcObject: null,
    pause() { this.paused = true; }, play() { return Promise.resolve(); }
  }]));
  let fetches = 0, peer;
  class FakePeer {
    constructor() { peer = this; this.connectionState = "new"; }
    addTrack() {}
    createDataChannel() { return this.dc = { readyState: "open", close() { this.closed = true; }, send() {} }; }
    async createOffer() { return { sdp: "v=0" }; }
    async setLocalDescription() {}
    async setRemoteDescription() {}
    close() { this.closed = true; }
  }
  vm.runInNewContext(script, {
    ToolController, document: { querySelector: k => nodes[k] },
    navigator: { mediaDevices: { getUserMedia: () => mediaPromise } },
    RTCPeerConnection: FakePeer,
    fetch: async () => { fetches++; return { ok: true, text: async () => "answer" }; },
    window: { addEventListener() {} },
    AbortController, setTimeout, clearTimeout, console
  });
  return { nodes, peer: () => peer, fetches: () => fetches };
}

// Stop while permission is pending; any late track is released without a session request.
{
  let allowMic;
  const page = fakePage(new Promise(resolve => { allowMic = resolve; }));
  const starting = page.nodes["#start"].onclick();
  page.nodes["#stop"].onclick();
  const track = { stopped: false, stop() { this.stopped = true; } };
  allowMic({ getTracks: () => [track], getAudioTracks: () => [track] });
  await starting;
  assert.equal(track.stopped, true);
  assert.equal(page.fetches(), 0);
  assert.equal(page.nodes["#start"].disabled, false);
}

// Stop after signaling: microphone, data channel, peer and playback all close.
{
  const track = { stopped: false, stop() { this.stopped = true; } };
  const page = fakePage(Promise.resolve({ getTracks: () => [track], getAudioTracks: () => [track] }));
  await page.nodes["#start"].onclick();
  page.nodes["#stop"].onclick();
  assert.equal(page.fetches(), 1);
  assert.equal(track.stopped, true);
  assert.equal(page.peer().closed, true);
  assert.equal(page.peer().dc.closed, true);
  assert.equal(page.nodes["#reply"].paused, true);
  assert.equal(page.nodes["#reply"].srcObject, null);
}

console.log("Synthetic browser lifecycle checks passed: late microphone and full Stop cleanup.");
```

அந்த அடைவில் சேவையகத்தின் ஒரே சார்புத் தொகுப்பை நிறுவி சேவையகத்தைத் தொடங்குங்கள்:

```bash
npm init -y
npm install express
node server.mjs
```

திறக்க வேண்டியது `http://localhost:3000`. பக்கத்தில் **Start voice**, **Stop and release microphone**, ஒலி இயக்கி, நிகழ்வு நிலை தெரிய வேண்டும். உலாவிப் பக்கமும் கட்டுப்படுத்தி தொகுதியும் மட்டுமே வழங்கப்படுகின்றன; இரண்டு சோதனைச் செயல்பாடுகள் உலாவியில் இயங்கி உண்மையான இடம், நாள்காட்டி, CRM அல்லது வணிகக் கணக்கை அணுகுவதில்லை.

## 2. தொடங்கி, கேட்டு, முடிவைச் சரிபாருங்கள்

சொடுக்குங்கள் **Start voice**, ஒலிவாங்கிக்கு அனுமதித்து **Connected** வரக் காத்திருங்கள். கற்பனைச் செயல்விளக்க நூலகத்தின் நேரம், முகவரி கேளுங்கள். உலாவி WebRTC ஊடகத் தடத்தில் ஒலிவாங்கிச் சத்தத்தை அனுப்பி, தொலைவிலிருந்து குரல் தடம் வந்ததும் ஒலி கட்டுப்பாட்டுடன் இணைக்கிறது. தானியக்க இயக்கம் தடுக்கப்பட்டால் தெரியும் கட்டுப்பாட்டில் **Play** அழுத்துங்கள். பயனுள்ளபோது சிறு முன்னேற்றச் செய்தியை முதலில் பேசுமாறு சேவையக அறிவுறுத்தல் கேட்கிறது. மாதிரி அதை விடலாம்; கேட்டதாலேயே தேடல் வெற்றியென்று நிரூபணமில்லை.

நிகழ்வுப் பலகம் சாவி, மூல ஒலி, எழுத்துப்பதிவைக் காட்டாமல் பதில் நிலைகள், அழைப்பு IDகள், வெளியீட்டு உறுதிகளைத் தருகிறது. மாதிரி கருவியைத் தேர்ந்தெடுக்காமல் இருக்கலாம், ஒன்று அல்லது பல தேர்ந்தெடுக்கலாம். `response.done` இல் `status: completed` இருந்தால் கட்டுப்படுத்தி **அனைத்து** வெளியீட்டு உறுப்புகளிலும் நிறைவுற்ற `function_call` உறுப்புகளைத் தேடி, ஒவ்வொரு `call_id` உள்ளது, தனித்துவமானது எனச் சரிபார்க்கிறது. அனுமதித்த தேடல்களைப் பல ஒரே நேரத்திலும் இயக்கி, ஒவ்வொரு அசல் IDக்கும் ஒரு `function_call_output` அனுப்புகிறது. தோல்வியான தேடல் அந்த அழைப்பின் பிழை முடிவாகும்; பிற முடிவுகள் தக்கவைக்கப்படும். ஒதுக்கிய ஒவ்வொரு வெளியீட்டு உறுப்பு IDக்கும் `conversation.item.done` வரக் காத்திருந்து, பின்னர் ஒரே `response.create` அனுப்புகிறது. அனைத்து உறுதிகளுக்குக் காத்திருப்பது எடுத்துக்காட்டின் எச்சரிக்கையான உள்ளூர் வரிசைத் தேர்வு; API விதி அல்ல. பதிலுக்கு எட்டு அழைப்புகள், குறுக்கீடு இல்லாத ஒரு உரையாடல் முறைக்கு மூன்று கருவிச் சுற்றுகள் அதிகபட்சம்; இவை உள்ளூர் வரம்புகள்.

கவனியுங்கள் **Realtime session created**, இணைந்த இணை, இருந்தால் கருவி அழைப்பு IDகள், வெளியீட்டு உறுதிகள், முடிந்த அடுத்த பதில். பதிலைக் கேட்டு நேரம், முகவரியை `index.html` உள்ள கற்பனைச் சரங்களுடன் ஒப்பிடுங்கள். பதில் நிறைவு நிகழ்வு மட்டும் கேட்கும் இயக்கம் அல்லது சரியான விடையை உறுதிப்படுத்தாது. இந்தக் கவனிப்புகளுக்கு உங்கள் கட்டண உலாவி இயக்கம் வேண்டும்; BIG CHANGE செய்யவில்லை.

## 3. அடுத்த முறை, இடைமறித்தல், ஒலிவாங்கி விடுவித்தலைச் சோதியுங்கள்

மறுதொடக்கம் இல்லாமல் மற்றொரு கேள்வி கேளுங்கள். இடைமறித்தலைச் சோதிக்க மாதிரி பேசும்போது அல்லது தேடல் காத்திருக்கும்போது பேசுங்கள். WebRTC, குரல் செயல்பாட்டு அறிதலுக்காக OpenAI ரத்துசெய்தல், இன்னும் இயக்காத ஒலியைச் சேவையகம் நிர்வகித்துக் குறைத்தல் ஆகியவற்றை ஆவணப்படுத்துகிறது. `input_audio_buffer.speech_started` வந்தால் உள்ளூர் கட்டுப்படுத்தியும் காத்திருக்கும் தேடல்களைச் செல்லாததாக்கும்; தாமத முடிவு இடைமறித்த பதிலை மீண்டும் தொடங்காது. இந்தக் குறியீடு WebSocket ஒலி இயக்கிக்குத் தேவையான தனிப் பின்னணி இயக்கச் சுட்டி, ஒலிக் குறைப்பு நிகழ்வுகளைச் செயல்படுத்தவில்லை.

சொடுக்குங்கள் **Stop and release microphone**; உலாவியின் ஒலிவாங்கி குறியீடு அணைந்ததா உறுதிசெய்யுங்கள். அனுமதி கோரிக்கை காத்திருக்கும்போதும் நிறுத்தலாம்; அனுமதி தாமதமாக வந்தால் கிடைக்கும் தடம் உடனே நிறுத்தப்படும். அதே துப்புரவு தடங்களை நிறுத்தி, நடப்பு அமர்வுக் கோரிக்கையை ரத்துசெய்து, தரவு வழி, இணையை மூடி, இயக்கத்தை நிறுத்திப் பிரித்து, கருவிப் பணியை ரத்துசெய்கிறது. பக்கத்தைவிட்டு வெளியேறுதல், அமைப்பு அல்லது உள்ளூர் அமர்வு நேரம் முடிதல், இணைப்பு தோல்வியில் நேரக் கணிப்பான்களை அகற்றுகிறது. எடுத்துக்காட்டில் அமைப்புக்கு 30 வினாடி, உள்ளூர் அமர்வுக்கு 55 நிமிடம்; OpenAI Realtime அதிகபட்சம் 60 நிமிடம் என்கிறது.

## 4. உள்ளூர் சோதனைகளை இயக்கி தோல்விகளைச் சரிசெய்யுங்கள்

அதே அடைவில் இயக்குங்கள்:

```bash
node --check server.mjs
node --check controller.mjs
node protocol-check.mjs
node browser-check.mjs
```

இரு நிரல்கள் செயற்கை நிகழ்வுகள், உலாவி மாதிரிகளைப் பயன்படுத்துகின்றன. கலந்த செய்தி/செயல்பாட்டு பதில், எதிர்வரிசையில் முடியும் இரு அழைப்புகள், ஒரே தொடர்ச்சிக்கு முன் அனைத்து வெளியீடு-உறுதிகள், அழைப்புவாரிப் பிழைத் தனிமை, நிறைவுற்ற அழைப்புகள் மட்டும் இயக்குதல், சுற்று வரம்புகள், பேச்சு இடைமறித்தல், தாமத அனுமதி விடுவித்தல், Stop துப்புரவு ஆகியவற்றைச் சோதிக்கின்றன. API கோரிக்கை செய்வதில்லை. தேர்ச்சி வலைப்பின்னல் நேரம், உண்மையான சாதனம், குரல் தரம் அல்லது உற்பத்திச் செயல்பாட்டு நம்பகத்தன்மையை அளவிடாது.

ஒருவேளை **Start** தோல்வியடைந்தால் தெரியும் நிலை, உள்ளூர் சேவையகம் பெற்ற மேல்நிலை HTTP நிலையைக் படியுங்கள். அனுமதி, `localhost:3000`, சேவையகச் சூழல் சாவி, மாதிரி அணுகல், கட்டணம், வரம்பு, வலைப்பின்னலைச் சரிபாருங்கள். 502 மேல்நிலை மறுப்பு, வலைத் தோல்வி அல்லது எடுத்துக்காட்டின் 25 வினாடி கோரிக்கை நேர எல்லையாகலாம்; OpenAI கோரிக்கையைப் பெறவில்லை என நிரூபிக்காது. இணைந்தும் ஒலி இல்லாவிட்டால் உள்ளீடு, வெளியீட்டுச் சாதனங்களைப் பார்த்து **Play** அழுத்துங்கள். தோல்வியான, முழுமையற்ற, ரத்தான பதில் பகுதி அழைப்புகளை இயக்காது. வெளியீட்டு உறுதி நேரம் முடிந்தால் கட்டுப்படுத்தி நிச்சயமற்ற இரட்டைத் தொடர்ச்சி அனுப்பாமல் அமர்வை மூடும். தொடக்கம் அல்லது வலை முடிவு தெரியாவிட்டால் அமர்வை நிறுத்தி ஒலிவாங்கி விடுவித்தலை உறுதிசெய்து, மறுமுயற்சிக்கு முன் திட்டப் பயன்பாட்டைப் பாருங்கள்; ரத்தான கோரிக்கைக்கும் ஏற்கெனவே செலவு ஏற்பட்டிருக்கலாம்.

## செலவும் வரம்புகளும்

OpenAI வழங்கும் [மாதிரிப் பக்கம்](https://developers.openai.com/api/docs/models/gpt-realtime-2.1) மற்றும் [விலைப் பட்டியல்](https://developers.openai.com/api/docs/pricing) 2026 அக்டோபர் 20 அன்று சரிபார்க்கப்பட்டன. தேக்கமில்லா உரை உள்ளீடு/வெளியீடு பத்து லட்சம் டோக்கன்களுக்கு 4/24 அமெரிக்க டாலர், ஒலி உள்ளீடு/வெளியீடு 32/64 டாலர். உண்மைச் செலவு டோக்கன், ஒலி, மீண்டும் வரும் உரையாடல் சூழலைச் சாரும். கணக்கு, தாமதம், நேரலை கருவி நடத்தை அல்லது தொடக்க முன்னேற்றச் செய்தியை அளவிடவில்லை. ஒலிவாங்கிச் சத்தம் OpenAI க்கு அனுப்பப்படும். நிலையான கற்பனைச் செயல்பாடுகள் உண்மை வணிக இணைப்புகள் அல்லது எழுதும் செயல்களை அனுமதிப்பதில்லை. பகிரப்பட்ட செயல்படுத்தலுக்கு தனிப் பயனர் அடையாளம், வரம்பு, கருவி அதிகாரம், கண்காணிப்பு, மதிப்பாய்வு வேண்டும்.

### மூலங்கள் மற்றும் மேலும் படிக்க

- [OpenAI WebRTC வழிகாட்டி](https://developers.openai.com/api/docs/guides/voice-webrtc): உலாவி ஊடகத் தடம், தொலைவிலிருந்து இயக்கம், தரவு வழி, நம்பக சேவையக SDP பரிமாற்றம். குறிப்பிட்ட உலாவி அல்லது கணக்கைச் சோதிக்கவில்லை.
- [Realtime உரையாடல்கள்](https://developers.openai.com/api/docs/guides/realtime-conversations): நிறைவுற்ற பதிலின் முழு வெளியீடு, செயல்பாட்டு முடிவு வரிசை, இடைமறித்தல், 60 நிமிட அமர்வு வரம்பு. குறிப்பிட்ட மாதிரி கருவி அழைக்கும் என உறுதியில்லை.
- [Realtime கருவிகள்](https://developers.openai.com/api/docs/guides/realtime-mcp): பயன்பாடு நிர்வகிக்கும் செயல்பாட்டு அழைப்புகள் மற்றும் `function_call_output`. இந்த உள்ளூர் சோதனைத் தரவின் வெற்றியை நிரூபிக்காது.
- [கிளையன்ட்](https://developers.openai.com/api/reference/resources/realtime/client-events) மற்றும் [சேவையக நிகழ்வுகள்](https://developers.openai.com/api/reference/resources/realtime/server-events) குறிப்புகள்: உறுப்பு IDகள், உறுதிகள், பதில் நிகழ்வு அமைப்புகள். அனைத்து உறுதிகளுக்கும் காத்திருப்பது உள்ளூர் தேர்வு.
- [Realtime அறிவுறுத்தல் எழுதுதல்](https://developers.openai.com/api/docs/guides/voice-prompting): நிபந்தனைக்குட்பட்ட குறுகிய குரல் முன்னேற்றச் செய்திக்கான வழிகாட்டுதல்; கேட்கும் வெளியீட்டின் உறுதி அல்ல.
- [மாதிரி அணுகல்](https://developers.openai.com/api/docs/models/gpt-realtime-2.1) மற்றும் [API விலைகள்](https://developers.openai.com/api/docs/pricing): தற்போதைய அணுகல் நிலைகள், டோக்கன் கட்டணங்கள். உண்மையான கட்டணத்தை அறிய திட்டப் பயன்பாட்டைப் பாருங்கள்.

## Sources

- [OpenAI WebRTC வழிகாட்டி](https://developers.openai.com/api/docs/guides/voice-webrtc) — உலாவி ஒலிவாங்கி, தொலைவிலிருந்து ஒலித் தடங்கள், oai-events தரவு வழி, /v1/realtime/calls மூலம் வழக்கமான API சாவியுடன் நம்பக சேவையக SDP பரிமாற்றம் விவரிக்கிறது. எடுத்துக்காட்டின் உண்மை சாதனம் அல்லது வலைப்பின்னல் நடத்தையை நிரூபிக்காது.
- [OpenAI Realtime உரையாடல்கள்](https://developers.openai.com/api/docs/guides/realtime-conversations) — response.done முழு வெளியீடு, செயல்பாட்டு முடிவுக்குப் பின் தொடர்ச்சி, இயக்காத ஒலியைச் சேவையகம் குறைக்கும் WebRTC VAD இடைமறித்தல், 60 நிமிட அதிகபட்சம் விவரிக்கிறது. வாசகர் இயக்கத்தில் கருவி பயன்பாடு அல்லது பேச்சின் துல்லியத்தை உறுதிப்படுத்தாது.
- [கருவிகளுடன் OpenAI Realtime](https://developers.openai.com/api/docs/guides/realtime-mcp) — பயன்பாட்டுச் செயல்பாட்டுக் கருவிகள், அசல் call_id கொண்ட function_call_output உறுப்புகள், response.create தொடர்ச்சியை விவரிக்கிறது. ஒவ்வொரு வெளியீட்டு உறுதிக்கும் காத்திருப்பது கூடுதல் உள்ளூர் வரிசைத் தேர்வு.
- [OpenAI Realtime கிளையன்ட் நிகழ்வுகள்](https://developers.openai.com/api/reference/resources/realtime/client-events) — எடுத்துக்காட்டின் conversation.item.create, உறுப்பு IDகள், function_call_output, response.create அமைப்புகளை வரையறுக்கிறது; குறிப்பு ஒருங்கிணைப்புச் சோதனை அல்ல.
- [OpenAI Realtime சேவையக நிகழ்வுகள்](https://developers.openai.com/api/reference/resources/realtime/server-events) — எடுத்துக்காட்டின் நிறைவுற்ற பதில், இறுதி உரையாடல் உறுப்பு உறுதி, பிழை நிகழ்வு அமைப்புகளை வரையறுக்கிறது. எல்லா உறுதிக்கும் காத்திருப்பது உள்ளூர் தேர்வு, API நிபந்தனை அல்லது ஒருங்கிணைப்புச் சோதனை அல்ல.
- [OpenAI குரல் செயல்பாட்டு அறிதல்](https://developers.openai.com/api/docs/guides/realtime-vad) — உள்ளூர் பணியைச் செல்லாததாக்க speech_started, speech_stopped குறிகளை விவரிக்கிறது; இங்கே உண்மையான இடைமறித்தலைச் சோதிக்கவில்லை.
- [OpenAI Realtime அறிவுறுத்தல் எழுதுதல்](https://developers.openai.com/api/docs/guides/voice-prompting) — நிபந்தனைக்குட்பட்ட சிறிய முன்னேற்றச் செய்தியைக் கோருவதை ஆதரிக்கிறது. கட்டண அமர்வில் செய்தி கவனிக்கப்படவில்லை.
- [OpenAI gpt-realtime-2.1 மாதிரி](https://developers.openai.com/api/docs/models/gpt-realtime-2.1) — மாதிரி ID, ஆதரிக்காத Free நிலை, தேக்கமில்லா பத்து லட்சம் டோக்கன் விலைகளைக் குறிப்பிடுகிறது: உரை உள்ளீடு/வெளியீடு 4/24 அமெரிக்க டாலர், ஒலி 32/64. அணுகல், கட்டணத்திற்கு வாசகர் கணக்கைச் சரிபார்க்க வேண்டும்.
- [OpenAI API விலைகள்](https://developers.openai.com/api/docs/pricing) — வெளியிட்ட gpt-realtime-2.1 உரை, ஒலி டோக்கன் விலைகளை உறுதிப்படுத்துகிறது; இந்த வழிகாட்டி அமர்வுக்கு நிலையான செலவு தருவதில்லை.
- [OpenAI API மற்றும் ChatGPT கட்டணம்](https://help.openai.com/en/articles/9039756-managing-billing-for-chatgpt-and-the-api-platform) — API பயன்பாடு ChatGPT சந்தாவிலிருந்து தனியாகக் கட்டணமிடப்படும் என OpenAI கூறுகிறது. வாசகரின் தற்போதைய மீதம் அல்லது திட்ட வரம்பை உறுதிப்படுத்தாது.