@braze/sanity-plugin-ai-translation 0.2.86 → 0.3.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.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  A Sanity Studio plugin that adds an AI-powered **Translate** document action for internationalized content.
4
4
 
5
5
  - **Default provider:** Sanity Agent Actions (`client.agent.action.translate()`)
6
- - **Optional provider:** OpenAI GPT-5 (direct API) for large documents / long style guides
6
+ - **Optional provider:** OpenAI GPT-5 (through a Sanity function) for large documents / long style guides
7
7
  - **Output:** Translations are created as **versioned documents inside Content Releases** (staged publishing)
8
8
  - **Extras:** translate referenced documents, glossary terms, protected phrases, debug logging, field exclusion, and field transforms (Portable Text + captions)
9
9
 
@@ -199,6 +199,8 @@ If Agent Actions fail with schema-related errors, re-check:
199
199
 
200
200
  OpenAI mode is optional. If enabled, users can choose **OpenAI** as the translation provider inside the translation dialog.
201
201
 
202
+ Studio never calls OpenAI or holds an OpenAI API key. It sends the assembled prompt to a deployed Sanity function with `client.functions.invoke(..., { sync: true })`, using the editor's Studio session. The function makes the OpenAI call with the [server entry](#server-entry) and returns the translated text.
203
+
202
204
  Example configuration:
203
205
 
204
206
  ```typescript
@@ -210,19 +212,25 @@ aiTranslationPlugin({
210
212
 
211
213
  openai: {
212
214
  enabled: true,
213
- apiKey: process.env.OPENAI_API_KEY!,
215
+ stackId: "ST-xxxxxxxx", // blueprint stack that contains the function (required)
216
+ functionName: "ai-translation",
214
217
  model: "gpt-5",
215
- reasoningEffort: "medium", // 'minimal' | 'low' | 'medium' | 'high'
216
- verbosity: "medium", // 'low' | 'medium' | 'high' (if supported by model)
218
+ reasoningEffort: "medium", // 'low' | 'medium' | 'high'
217
219
  defaultMode: "openai-only", // 'sanity-only' | 'openai-only'
218
220
  },
219
221
  });
220
222
  ```
221
223
 
224
+ **Requirements**
225
+
226
+ - Studio's Sanity client must be `@sanity/client` 8.1 or newer, which adds `client.functions.invoke`. If it is older, the dialog shows OpenAI mode as unavailable.
227
+ - A pubsub function with the configured name must be deployed in the stack. It reads `OPENAI_API_KEY` from its own environment, allowlists models, and returns the result of `requestOpenAICompletion`.
228
+ - The function timeout must be long enough for a reasoning call. Use at least 120 seconds; Sanity allows up to 900. The plugin waits up to 900 seconds for the invocation.
229
+
222
230
  **Security**
223
231
 
224
- - Do not hardcode API keys.
225
- - Use environment variables (e.g. `.env` / `.env.local`) and your deployment platform’s secret manager.
232
+ - Do not put an OpenAI API key in the Studio config or bundle. It belongs only in the function environment.
233
+ - The payload sent to the function contains only the messages, model, and reasoning effort. It is client-supplied, so the function must validate it and must not treat any field in it as an identity check.
226
234
 
227
235
  **Why use OpenAI mode?**
228
236
 
@@ -234,6 +242,27 @@ aiTranslationPlugin({
234
242
 
235
243
  - OpenAI calls incur API costs based on token usage.
236
244
 
245
+ ### Server entry
246
+
247
+ The plugin exposes a server-only entry at `@braze/sanity-plugin-ai-translation/server` for running the OpenAI call outside the browser (for example in a Sanity function). It does not load React or Studio, it is not part of the Studio bundle, and it does not read the API key from the environment, so the caller passes it in.
248
+
249
+ ```typescript
250
+ import { requestOpenAICompletion } from "@braze/sanity-plugin-ai-translation/server";
251
+
252
+ // payload: { model, messages, reasoningEffort?, temperature? } assembled in Studio
253
+ const { content, tokensUsed } = await requestOpenAICompletion(payload, {
254
+ apiKey: process.env.OPENAI_API_KEY!,
255
+ });
256
+ ```
257
+
258
+ The first argument is the client-supplied request and is validated at runtime. The second holds server-side configuration such as the API key, and is never read from the payload.
259
+
260
+ - Requests with more than 20 messages, or more than 1,000,000 characters of message content in total, are rejected before anything is sent to OpenAI.
261
+ - Only `role` and `content` are sent for each message. Any other fields in the payload are dropped.
262
+ - Logs never include prompt or document text, even with plugin logging turned on. They include only the model, message roles, and content lengths.
263
+
264
+ Serialization, prompt assembly, structure validation, and the Content Release write stay in Studio. The entry does not allowlist models. Do that in the caller before invoking it.
265
+
237
266
  ---
238
267
 
239
268
  ## User guide (for editors)
@@ -438,18 +467,18 @@ aiTranslationPlugin({
438
467
 
439
468
  ### Enable debug logging
440
469
 
441
- This plugin uses a centralized debug logger. To enable debug output in your browser:
442
-
443
- ```js
444
- localStorage.setItem("sanity-ai-translation:debug", "true");
445
- ```
470
+ This plugin uses a centralized debug logger. Output is off by default. To turn it on, set this environment variable in the host Studio, then restart the dev server or rebuild (the value is inlined at build time):
446
471
 
447
- To disable:
472
+ ```sh
473
+ # Standalone Sanity Studio
474
+ SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING=true
448
475
 
449
- ```js
450
- localStorage.removeItem("sanity-ai-translation:debug");
476
+ # Studio embedded in Next.js
477
+ NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING=true
451
478
  ```
452
479
 
480
+ To turn it off, remove the variable or set it to any other value.
481
+
453
482
  ### Common issues
454
483
 
455
484
  **Agent Actions fail immediately**
@@ -492,6 +521,10 @@ pnpm typecheck
492
521
  pnpm lint
493
522
  ```
494
523
 
524
+ CI (`.github/workflows/ci.yml`) runs `biome ci`, the plugin typecheck, the test suite, and the plugin build on every pull request and on pushes to `main`.
525
+
526
+ To check the [server entry](#server-entry) against the real OpenAI API, run `pnpm check:openai-server` from the repo root or from `package/`. It reads `OPENAI_API_KEY` from the environment or from a `.env` file. See `package/scripts/README.md` for details.
527
+
495
528
  ---
496
529
 
497
530
  ## License
@@ -0,0 +1,142 @@
1
+ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true" || process.env.SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === "true", devLog = {
2
+ info: (message, context) => {
3
+ isLoggingEnabled() && console.info("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
4
+ },
5
+ warn: (message, context) => {
6
+ isLoggingEnabled() && console.warn("[AI Translation]", message, context ? JSON.stringify(context, null, 2) : "");
7
+ },
8
+ error: (message, error, context) => {
9
+ isLoggingEnabled() && console.error("[AI Translation]", message, error, context ? JSON.stringify(context, null, 2) : "");
10
+ },
11
+ debug: (message, data, context) => {
12
+ isLoggingEnabled() && console.debug("[AI Translation]", message, data, context ? JSON.stringify(context, null, 2) : "");
13
+ },
14
+ group: (label, context) => {
15
+ isLoggingEnabled() && console.group(`[AI Translation] ${label}`, context ? JSON.stringify(context, null, 2) : "");
16
+ },
17
+ groupEnd: () => {
18
+ isLoggingEnabled() && console.groupEnd();
19
+ },
20
+ time: (label) => {
21
+ isLoggingEnabled() && console.time(`[AI Translation] ${label}`);
22
+ },
23
+ timeEnd: (label) => {
24
+ isLoggingEnabled() && console.timeEnd(`[AI Translation] ${label}`);
25
+ }
26
+ };
27
+ function isNetworkError(error) {
28
+ if (!error) return !1;
29
+ if (error instanceof TypeError) {
30
+ const message = error.message.toLowerCase();
31
+ return message.includes("fetch") || message.includes("network") || message.includes("connection") || message.includes("failed to fetch") || message.includes("networkerror");
32
+ }
33
+ if (error && typeof error == "object" && "code" in error) {
34
+ const code = error.code;
35
+ return ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "ECONNREFUSED", "EHOSTUNREACH"].includes(code);
36
+ }
37
+ if (error && typeof error == "object" && "response" in error) {
38
+ const response = error.response;
39
+ if (response?.status)
40
+ return response.status >= 500 || response.status === 429;
41
+ }
42
+ if (error instanceof Error) {
43
+ const message = error.message.toLowerCase();
44
+ if (/\b(50[0-9]|429)\b/.test(message) || message.includes("rate limit"))
45
+ return !0;
46
+ }
47
+ return !1;
48
+ }
49
+ function isSanityMutationError(error) {
50
+ if (!error) return !1;
51
+ if (error instanceof Error) {
52
+ const message = error.message.toLowerCase();
53
+ return [
54
+ "validation",
55
+ "schema",
56
+ "invalid document",
57
+ "invalid field",
58
+ "type mismatch",
59
+ "reference",
60
+ "patch",
61
+ "mutation",
62
+ "document structure",
63
+ "required field",
64
+ "field type",
65
+ "sanity"
66
+ ].some((pattern) => message.includes(pattern));
67
+ }
68
+ if (error && typeof error == "object") {
69
+ const errorObj = error;
70
+ if (("statusCode" in errorObj || "response" in errorObj || "details" in errorObj) && "statusCode" in errorObj) {
71
+ const statusCode = errorObj.statusCode;
72
+ if (statusCode >= 400 && statusCode < 500 && statusCode !== 429)
73
+ return !0;
74
+ }
75
+ }
76
+ return !1;
77
+ }
78
+ const DEFAULT_RETRY_OPTIONS = {
79
+ maxAttempts: 2,
80
+ initialDelayMs: 0,
81
+ maxDelayMs: 5e3,
82
+ backoffMultiplier: 2,
83
+ retryableErrors: isNetworkError,
84
+ logRetries: !0
85
+ };
86
+ function calculateDelay(attempt, options) {
87
+ const delay = options.initialDelayMs * options.backoffMultiplier ** attempt;
88
+ return Math.min(delay, options.maxDelayMs);
89
+ }
90
+ function sleep(ms) {
91
+ return new Promise((resolve) => setTimeout(resolve, ms));
92
+ }
93
+ async function retryWithBackoff(operation, options = {}) {
94
+ const opts = {
95
+ ...DEFAULT_RETRY_OPTIONS,
96
+ ...options,
97
+ retryableErrors: options.retryableErrors || DEFAULT_RETRY_OPTIONS.retryableErrors
98
+ };
99
+ let lastError;
100
+ const maxAttempts = opts.maxAttempts + 1;
101
+ for (let attempt = 1; attempt <= maxAttempts; attempt++)
102
+ try {
103
+ const result = await operation();
104
+ return attempt > 1 && opts.logRetries && devLog.info(`Operation succeeded after ${attempt} attempts`, {
105
+ ...opts.context,
106
+ attempt,
107
+ totalAttempts: attempt
108
+ }), result;
109
+ } catch (error) {
110
+ if (lastError = error, !opts.retryableErrors(error))
111
+ throw opts.logRetries && devLog.warn("Error is not retryable, aborting retries", {
112
+ ...opts.context,
113
+ attempt,
114
+ error: error instanceof Error ? error.message : String(error)
115
+ }), error;
116
+ if (attempt >= maxAttempts)
117
+ throw opts.logRetries && devLog.error(
118
+ `Operation failed after ${maxAttempts} attempts`,
119
+ error instanceof Error ? error : new Error(String(error)),
120
+ {
121
+ ...opts.context,
122
+ totalAttempts: maxAttempts
123
+ }
124
+ ), error;
125
+ const delay = calculateDelay(attempt - 1, opts);
126
+ opts.logRetries && devLog.warn(`Operation failed, retrying in ${delay}ms (attempt ${attempt}/${maxAttempts})`, {
127
+ ...opts.context,
128
+ attempt,
129
+ totalAttempts: maxAttempts,
130
+ delayMs: delay,
131
+ error: error instanceof Error ? error.message : String(error)
132
+ }), delay > 0 && await sleep(delay);
133
+ }
134
+ throw lastError;
135
+ }
136
+ export {
137
+ devLog,
138
+ isNetworkError,
139
+ isSanityMutationError,
140
+ retryWithBackoff
141
+ };
142
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sources":["../../src/utils/common/devLog.ts","../../src/utils/common/retry.ts"],"sourcesContent":["/**\n * Development logging utility for AI Translation plugin troubleshooting\n * Only logs when SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING environment variable is \"true\"\n */\n\nconst isLoggingEnabled = (): boolean => {\n return (\n process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === 'true' ||\n process.env.SANITY_STUDIO_AI_TRANSLATION_PLUGIN_LOGGING === 'true'\n )\n}\n\nexport interface DevLogContext {\n component?: string\n action?: string\n documentId?: string\n language?: string\n jobId?: string\n // biome-ignore lint/suspicious/noExplicitAny: not worth the effort\n [key: string]: any\n}\n\nexport const devLog = {\n info: (message: string, context?: DevLogContext) => {\n if (!isLoggingEnabled()) return\n console.info('[AI Translation]', message, context ? JSON.stringify(context, null, 2) : '')\n },\n\n warn: (message: string, context?: DevLogContext) => {\n if (!isLoggingEnabled()) return\n console.warn('[AI Translation]', message, context ? JSON.stringify(context, null, 2) : '')\n },\n\n error: (message: string, error?: Error | unknown, context?: DevLogContext) => {\n if (!isLoggingEnabled()) return\n console.error('[AI Translation]', message, error, context ? JSON.stringify(context, null, 2) : '')\n },\n\n debug: (message: string, data?: unknown, context?: DevLogContext) => {\n if (!isLoggingEnabled()) return\n console.debug('[AI Translation]', message, data, context ? JSON.stringify(context, null, 2) : '')\n },\n\n group: (label: string, context?: DevLogContext) => {\n if (!isLoggingEnabled()) return\n console.group(`[AI Translation] ${label}`, context ? JSON.stringify(context, null, 2) : '')\n },\n\n groupEnd: () => {\n if (!isLoggingEnabled()) return\n console.groupEnd()\n },\n\n time: (label: string) => {\n if (!isLoggingEnabled()) return\n console.time(`[AI Translation] ${label}`)\n },\n\n timeEnd: (label: string) => {\n if (!isLoggingEnabled()) return\n console.timeEnd(`[AI Translation] ${label}`)\n },\n}\n","import { devLog } from './devLog'\n\n/**\n * Error classification utilities for determining retry strategies\n */\n\n/**\n * Checks if an error is a network error that should be retried immediately\n */\nexport function isNetworkError(error: unknown): boolean {\n if (!error) return false\n\n // Check for fetch TypeError (network failures)\n if (error instanceof TypeError) {\n const message = error.message.toLowerCase()\n return (\n message.includes('fetch') ||\n message.includes('network') ||\n message.includes('connection') ||\n message.includes('failed to fetch') ||\n message.includes('networkerror')\n )\n }\n\n // Check for Node.js network error codes\n if (error && typeof error === 'object' && 'code' in error) {\n const code = (error as { code: string }).code\n return ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED', 'EHOSTUNREACH'].includes(code)\n }\n\n // Check for HTTP error responses\n if (error && typeof error === 'object' && 'response' in error) {\n const response = (error as { response: { status?: number } }).response\n if (response?.status) {\n // Retry on 5xx server errors and 429 rate limiting\n return response.status >= 500 || response.status === 429\n }\n }\n\n // Check for HTTP status in error message\n if (error instanceof Error) {\n const message = error.message.toLowerCase()\n // Check for 5xx or 429 in error message\n if (/\\b(50[0-9]|429)\\b/.test(message)) {\n return true\n }\n // Check for rate limit error messages\n if (message.includes('rate limit')) {\n return true\n }\n }\n\n return false\n}\n\n/**\n * Checks if an error is a Sanity API mutation error\n * These errors typically indicate invalid document structure or schema violations\n */\nexport function isSanityMutationError(error: unknown): boolean {\n if (!error) return false\n\n if (error instanceof Error) {\n const message = error.message.toLowerCase()\n\n // Common Sanity error patterns\n const sanityErrorPatterns = [\n 'validation',\n 'schema',\n 'invalid document',\n 'invalid field',\n 'type mismatch',\n 'reference',\n 'patch',\n 'mutation',\n 'document structure',\n 'required field',\n 'field type',\n 'sanity',\n ]\n\n return sanityErrorPatterns.some((pattern) => message.includes(pattern))\n }\n\n // Check for Sanity-specific error structure\n if (error && typeof error === 'object') {\n const errorObj = error as Record<string, unknown>\n // Sanity errors often have these properties\n if ('statusCode' in errorObj || 'response' in errorObj || 'details' in errorObj) {\n // Check if it's a client error (4xx) that's not a network issue\n if ('statusCode' in errorObj) {\n const statusCode = errorObj.statusCode as number\n if (statusCode >= 400 && statusCode < 500 && statusCode !== 429) {\n return true\n }\n }\n }\n }\n\n return false\n}\n\n/**\n * Checks if an error is an OpenAI API error\n */\nexport function isOpenAIError(error: unknown): boolean {\n if (!error) return false\n\n if (error instanceof Error) {\n const message = error.message.toLowerCase()\n return message.includes('openai') || message.includes('gpt-5 api error') || message.includes('api error')\n }\n\n return false\n}\n\n/**\n * Configuration options for retry logic\n */\nexport interface RetryOptions {\n /** Maximum number of retry attempts (default: 2) */\n maxAttempts?: number\n /** Initial delay in milliseconds before first retry (default: 0 for immediate) */\n initialDelayMs?: number\n /** Maximum delay in milliseconds (default: 5000) */\n maxDelayMs?: number\n /** Backoff multiplier (default: 2) */\n backoffMultiplier?: number\n /** Custom function to determine if error is retryable (default: isNetworkError) */\n retryableErrors?: (error: unknown) => boolean\n /** Context for logging */\n context?: Record<string, unknown>\n /** Whether to log retry attempts */\n logRetries?: boolean\n}\n\n/**\n * Default retry options\n */\nconst DEFAULT_RETRY_OPTIONS: Required<Omit<RetryOptions, 'context' | 'retryableErrors'>> & {\n retryableErrors: (error: unknown) => boolean\n} = {\n maxAttempts: 2,\n initialDelayMs: 0,\n maxDelayMs: 5000,\n backoffMultiplier: 2,\n retryableErrors: isNetworkError,\n logRetries: true,\n}\n\n/**\n * Calculates delay for exponential backoff\n */\nfunction calculateDelay(\n attempt: number,\n options: Required<Omit<RetryOptions, 'context' | 'retryableErrors'>> & {\n retryableErrors: (error: unknown) => boolean\n },\n): number {\n const delay = options.initialDelayMs * options.backoffMultiplier ** attempt\n return Math.min(delay, options.maxDelayMs)\n}\n\n/**\n * Waits for the specified number of milliseconds\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Retries an operation with exponential backoff\n *\n * @param operation - The async operation to retry\n * @param options - Retry configuration options\n * @returns The result of the operation\n * @throws The last error if all retries are exhausted\n *\n * @example\n * ```typescript\n * const result = await retryWithBackoff(\n * () => fetch('https://api.example.com/data'),\n * {\n * maxAttempts: 3,\n * initialDelayMs: 100,\n * context: { documentId: 'doc123' }\n * }\n * )\n * ```\n */\nexport async function retryWithBackoff<T>(operation: () => Promise<T>, options: RetryOptions = {}): Promise<T> {\n const opts = {\n ...DEFAULT_RETRY_OPTIONS,\n ...options,\n retryableErrors: options.retryableErrors || DEFAULT_RETRY_OPTIONS.retryableErrors,\n }\n\n let lastError: unknown\n const maxAttempts = opts.maxAttempts + 1 // +1 for initial attempt\n\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n const result = await operation()\n if (attempt > 1 && opts.logRetries) {\n devLog.info(`Operation succeeded after ${attempt} attempts`, {\n ...opts.context,\n attempt,\n totalAttempts: attempt,\n })\n }\n return result\n } catch (error) {\n lastError = error\n\n // Check if error is retryable\n if (!opts.retryableErrors(error)) {\n if (opts.logRetries) {\n devLog.warn('Error is not retryable, aborting retries', {\n ...opts.context,\n attempt,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n throw error\n }\n\n // If this was the last attempt, throw the error\n if (attempt >= maxAttempts) {\n if (opts.logRetries) {\n devLog.error(\n `Operation failed after ${maxAttempts} attempts`,\n error instanceof Error ? error : new Error(String(error)),\n {\n ...opts.context,\n totalAttempts: maxAttempts,\n },\n )\n }\n throw error\n }\n\n // Calculate delay and wait before retrying\n const delay = calculateDelay(attempt - 1, opts)\n if (opts.logRetries) {\n devLog.warn(`Operation failed, retrying in ${delay}ms (attempt ${attempt}/${maxAttempts})`, {\n ...opts.context,\n attempt,\n totalAttempts: maxAttempts,\n delayMs: delay,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n\n if (delay > 0) {\n await sleep(delay)\n }\n }\n }\n\n // This should never be reached, but TypeScript needs it\n throw lastError\n}\n"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -304,8 +304,19 @@ export declare interface PluginConfig {
304
304
  schemaId: string;
305
305
  maxConcurrentTranslationJobs?: number;
306
306
  maxReferenceDepth?: number;
307
+ /**
308
+ * OpenAI mode. Studio never calls OpenAI directly: it invokes a deployed Sanity function
309
+ * (which holds the API key) with the editor's session via `client.functions.invoke`.
310
+ * Requires `@sanity/client` 8.1 or newer as Studio's client.
311
+ */
307
312
  openai?: {
308
- apiKey: string;
313
+ /** Blueprint stack that contains the translation function, e.g. `ST-bfgaj9tnk7`. Required for OpenAI mode. */
314
+ stackId: string;
315
+ /**
316
+ * Name of the translation function, as declared in the blueprint.
317
+ * @defaultValue 'ai-translation'
318
+ */
319
+ functionName?: string;
309
320
  model?:
310
321
  | "gpt-5.1"
311
322
  | "gpt-5"