skills/runtime/references/transcription.md
Subclass TranscriptionService, pass an instance to CopilotRuntime({ transcriptionService }),
and the POST /transcribe endpoint lights up. The service has a single method,
transcribeFile, that returns the transcript as a plain string.
import {
CopilotRuntime,
createCopilotRuntimeHandler,
TranscriptionService,
type TranscribeFileOptions,
} from "@copilotkit/runtime/v2";
import OpenAI from "openai";
class OpenAIWhisperTranscription extends TranscriptionService {
private client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async transcribeFile({ audioFile }: TranscribeFileOptions): Promise<string> {
const result = await this.client.audio.transcriptions.create({
file: audioFile,
model: "whisper-1",
});
return result.text;
}
}
const runtime = new CopilotRuntime({
agents: {
/* ... */
} as any,
transcriptionService: new OpenAIWhisperTranscription(),
});
const handler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
});
export default { fetch: handler };
// packages/runtime/src/v2/runtime/transcription-service/transcription-service.ts
export interface TranscribeFileOptions {
audioFile: File;
mimeType?: string;
size?: number;
}
export abstract class TranscriptionService {
abstract transcribeFile(options: TranscribeFileOptions): Promise<string>;
}
Multipart (REST mode):
const form = new FormData();
form.append("audio", blob, "recording.webm");
await fetch("/api/copilotkit/transcribe", { method: "POST", body: form });
JSON (works in both multi-route and single-endpoint modes — dispatch is by
Content-Type: application/json; mimeType is required in the payload):
await fetch("/api/copilotkit/transcribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
audio: base64String,
mimeType: "audio/webm",
filename: "recording.webm", // optional
}),
});
class OpenAIWhisperTranscription extends TranscriptionService {
private client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async transcribeFile({
audioFile,
size,
}: TranscribeFileOptions): Promise<string> {
const max = 25 * 1024 * 1024; // 25 MB
if ((size ?? audioFile.size) > max) {
// "too long" keyword → audio_too_long response
throw new Error("Audio duration too long — max 25MB per upload");
}
const result = await this.client.audio.transcriptions.create({
file: audioFile,
model: "whisper-1",
});
return result.text;
}
}
The runtime inspects String(error).toLowerCase() thrown by your service and maps keywords
to error codes. Let the provider error bubble up — do not re-categorize inside the service.
| Keyword substrings | Maps to |
|---|---|
rate, 429, too many | rate_limited (retryable) |
auth, 401, api key, unauthorized | auth_failed (not retryable) |
too long, duration, length | audio_too_long (not retryable) |
| (anything else) | provider_error (retryable) |
Full error-code enum:
// packages/shared/src/transcription-errors.ts
export enum TranscriptionErrorCode {
SERVICE_NOT_CONFIGURED = "service_not_configured",
INVALID_AUDIO_FORMAT = "invalid_audio_format",
AUDIO_TOO_LONG = "audio_too_long",
AUDIO_TOO_SHORT = "audio_too_short",
RATE_LIMITED = "rate_limited",
AUTH_FAILED = "auth_failed",
PROVIDER_ERROR = "provider_error",
NETWORK_ERROR = "network_error",
INVALID_REQUEST = "invalid_request",
}
Wrong:
new CopilotRuntime({ agents });
// client calls /api/copilotkit/transcribe → 503
Correct:
new CopilotRuntime({
agents,
transcriptionService: new MyWhisperService(),
});
Unconfigured runtime returns HTTP 503 with
{ error: "service_not_configured" }. The frontend gets no transcript with no obvious
server-side failure.
Source: packages/runtime/src/v2/runtime/handlers/handle-transcribe.ts:203-207.
Wrong:
const form = new FormData();
form.append("file", blob, "recording.webm");
await fetch("/api/copilotkit/transcribe", { method: "POST", body: form });
Correct:
const form = new FormData();
form.append("audio", blob, "recording.webm");
await fetch("/api/copilotkit/transcribe", { method: "POST", body: form });
The handler reads formData.get("audio") — any other field name yields null and returns
invalid_request.
Source: packages/runtime/src/v2/runtime/handlers/handle-transcribe.ts:91-97.
Wrong:
await fetch("/api/copilotkit/transcribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ audio: b64 }),
});
Correct:
await fetch("/api/copilotkit/transcribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ audio: b64, mimeType: "audio/webm" }),
});
JSON mode requires mimeType — the handler explicitly rejects payloads missing it with
invalid_request.
Source: packages/runtime/src/v2/runtime/handlers/handle-transcribe.ts:131-136.
Wrong:
class MyService extends TranscriptionService {
async transcribeFile(opts: TranscribeFileOptions): Promise<string> {
try {
return await doTranscribe(opts);
} catch (e) {
// trying to hand-pick error codes
throw new Error("RATE_LIMITED");
}
}
}
Correct:
class MyService extends TranscriptionService {
async transcribeFile(opts: TranscribeFileOptions): Promise<string> {
return doTranscribe(opts); // let provider errors bubble up verbatim
}
}
The runtime scans String(error).toLowerCase() for "rate", "429", "auth", "too long"
etc. Provider-native messages ("OpenAI returned 429 rate limited") auto-map to the right
code. Hand-crafted codes bypass the keyword matcher and end up as provider_error.
Source: packages/runtime/src/v2/runtime/handlers/handle-transcribe.ts:160-196.
Wrong:
class MyService extends TranscriptionService {
async transcribeFile(opts: TranscribeFileOptions): Promise<string> {
// @ts-expect-error returning the wrong shape
return {
text: "hi",
segments: [
/* ... */
],
};
}
}
Correct:
class MyService extends TranscriptionService {
async transcribeFile(opts: TranscribeFileOptions): Promise<string> {
const result = await provider.transcribe(opts.audioFile);
return result.text;
}
}
transcribeFile returns Promise<string>. The handler sends
{ transcription: string } back to the client — any other shape is a TypeScript error and
would be JSON-stringified wrongly at runtime.
Source: packages/runtime/src/v2/runtime/transcription-service/transcription-service.ts:9-11.
copilotkit/setup-endpoint — /transcribe is one of the routes the handler mountscopilotkit/debug-and-troubleshoot — TranscriptionErrorCode catalog