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
68 lines
3.0 KiB
TypeScript
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;
|
|
}
|