Claude ebac3ef9d1
fix(media): disclose the transcription server before dictation uses it
Dictation falls back to the owner's transcription server when the voice
provider cannot transcribe, so that server is a voice recipient too: it is
listed for voice use (as "Transcription (<origin>)") whenever it would take
the recording, and not when the voice provider transcribes itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 14:26:54 +00:00

68 lines
3.0 KiB
TypeScript

import { createHash } from "node:crypto";
import type { AiDataUse, AiRecipient } from "@rakazo/contracts";
import { BUILTIN_MEMORY_PROVIDER_ID } from "./builtin-memory-provider.js";
import { MEDIA_COMFYUI_PROVIDER_ID, MEDIA_TRANSCRIPTION_PROVIDER_ID } from "./media-settings.js";
import { localBaseUrl } from "./pi-local-provider.js";
import { listPiCatalog } from "./pi-models.js";
import { voiceCatalogEntry } from "./voice-factory.js";
const PRIVACY_URLS: Record<string, string> = {
cursor: "https://cursor.com/privacy",
openrouter: "https://openrouter.ai/privacy",
openai: "https://openai.com/policies/privacy-policy/",
"openai-codex": "https://openai.com/policies/privacy-policy/",
anthropic: "https://www.anthropic.com/legal/privacy",
google: "https://policies.google.com/privacy",
"vercel-ai-gateway": "https://vercel.com/legal/privacy-policy",
elevenlabs: "https://elevenlabs.io/privacy-policy",
cartesia: "https://cartesia.ai/legal/privacy",
"fish-audio": "https://fish.audio/privacy/",
supermemory: "https://supermemory.ai/privacy",
};
/** Never expose endpoint paths, query parameters, user info, or credentials in a disclosure. */
export function aiRecipient(input: {
provider: string;
use: AiDataUse;
modelId?: string;
baseUrl?: string;
}): Omit<AiRecipient, "allowed"> | null {
if (input.provider === "scripted") return null;
const endpoint = input.baseUrl ?? (input.provider === "local" ? localBaseUrl() : undefined);
const origin = endpoint ? new URL(endpoint).origin : undefined;
const name = recipientName(input.provider, input.use);
// Bind custom connections to the full endpoint without exposing private URL components.
const key = createHash("sha256")
.update(JSON.stringify([input.use, input.provider, endpoint ?? "", input.modelId ?? ""]))
.digest("hex");
return {
key,
name: origin ? `${name} (${origin})` : name,
use: input.use,
detail: [
input.modelId ? `Model: ${input.modelId}.` : "",
["openrouter", "vercel-ai-gateway"].includes(input.provider)
? `${name} forwards requests to model providers using the routing and privacy settings configured for this connection.`
: "",
]
.filter(Boolean)
.join(" "),
privacyUrl: PRIVACY_URLS[input.provider],
};
}
function recipientName(provider: string, use: AiDataUse) {
// The transcription server also takes dictation, so it keeps its name for voice use.
if (provider === MEDIA_TRANSCRIPTION_PROVIDER_ID) return "Transcription";
if (use === "voice") return voiceCatalogEntry(provider)?.name ?? provider;
if (use === "memory") {
if (provider === "supermemory") return "Supermemory";
// Built-in memory keeps items in this deployment; its texts go to the embeddings server.
if (provider === BUILTIN_MEMORY_PROVIDER_ID) return "Embeddings";
return provider;
}
if (provider === "cursor") return "Cursor";
if (provider === MEDIA_COMFYUI_PROVIDER_ID) return "ComfyUI";
return listPiCatalog().find((entry) => entry.provider === provider)?.providerName ?? provider;
}