@focus-reactive/payload-plugin-translator 0.11.4 → 0.12.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 +1 -1
- package/dist/core/domain/translation-providers/failureReason.d.ts +12 -0
- package/dist/core/domain/translation-providers/failureReason.js +21 -0
- package/dist/core/translation-pipeline/stages/field-collector/FieldChunkCollector.js +4 -1
- package/dist/server/modules/provenance/Provenance.service.d.ts +6 -3
- package/dist/server/modules/provenance/Provenance.service.js +6 -3
- package/dist/server/shared/http/index.d.ts +1 -1
- package/dist/server/shared/http/index.js +1 -1
- package/dist/server/shared/http/toClientErrorMessage.d.ts +7 -4
- package/dist/server/shared/http/toClientErrorMessage.js +15 -4
- package/dist/server/shared/http/withErrorHandler.js +2 -1
- package/dist/translation-providers/openai/OpenAITranslation.provider.d.ts +1 -1
- package/dist/translation-providers/openai/OpenAITranslation.provider.js +1 -1
- package/dist/translation-providers/openai/openAIComplete.js +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -340,7 +340,7 @@ Pass **either** an `apiKey` **or** a ready-made `client` — never both; the typ
|
|
|
340
340
|
| -------------- | ------------------------- | ------------------ | -------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
341
341
|
| `apiKey` | `string` | Unless `client` | — | OpenAI API key. The `openai` package is loaded on first translation, not at config load. _Since v0.11.0: optional when `client` is given._ |
|
|
342
342
|
| `client` | `OpenAIClientShape` | Unless `apiKey` | — | Your own client — Azure OpenAI, a corporate proxy, OpenRouter, anything with a matching `chat.completions.create`. On this path the `openai` package is never loaded. _Since v0.11.0._ |
|
|
343
|
-
| `model` | `string` | No | `'gpt-
|
|
343
|
+
| `model` | `string` | No | `'gpt-5.4-mini'` | Model used for translation. **The default may change in a minor release** — pin it if you need reproducible output and cost. |
|
|
344
344
|
| `systemPrompt` | `SystemPromptBuilder` | No | Built-in prompt | Custom system-prompt builder. |
|
|
345
345
|
| `dryRun` | `boolean \| DryRunConfig` | No | `false` | **Deprecated** — see the note below. Simulates translations without API calls, but still writes, publishes and records provenance. |
|
|
346
346
|
| `sampling` | `OpenAISamplingParams` | No | not sent | `temperature`, `top_p`, `frequency_penalty`, `presence_penalty`. Omitted entirely unless set — several models reject them. **Before v0.11.0 this package always sent `temperature: 0, top_p: 1, frequency_penalty: 0, presence_penalty: 0`**, so translations were deterministic; they now follow the model's defaults. Set `{ temperature: 0 }` to restore that. _Since v0.11.0._ |
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payload's job queue keeps only the message: it rethrows a handler failure as
|
|
3
|
+
* `new TaskError({ message: err.message })` and persists `{ name, cancelled, message, stack }`,
|
|
4
|
+
* so the error's class, `code` and `cause` are gone before anything is stored (Payload 3.x).
|
|
5
|
+
* That is why a reason travels inside the string rather than as an error subclass or a field.
|
|
6
|
+
*/
|
|
7
|
+
/** A failure cause safe to name to an end user. Every member obliges admin-facing copy for it. */
|
|
8
|
+
export type UserFacingFailureReason = "model-unavailable";
|
|
9
|
+
/** `detail` is for logs and the job record; it is never shown to a user. */
|
|
10
|
+
export declare function markFailureReason(reason: UserFacingFailureReason, detail: string): string;
|
|
11
|
+
/** Anchored: a marked message quoted inside another error's text is not that error's own reason. */
|
|
12
|
+
export declare function readFailureReason(message: string): UserFacingFailureReason | null;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payload's job queue keeps only the message: it rethrows a handler failure as
|
|
3
|
+
* `new TaskError({ message: err.message })` and persists `{ name, cancelled, message, stack }`,
|
|
4
|
+
* so the error's class, `code` and `cause` are gone before anything is stored (Payload 3.x).
|
|
5
|
+
* That is why a reason travels inside the string rather than as an error subclass or a field.
|
|
6
|
+
*/ /** A failure cause safe to name to an end user. Every member obliges admin-facing copy for it. */ const MARKER_PREFIX = "translator:";
|
|
7
|
+
const MARKER = new RegExp(`^\\[${MARKER_PREFIX}([a-z-]+)\\] `, "u");
|
|
8
|
+
const REASONS = new Set([
|
|
9
|
+
"model-unavailable"
|
|
10
|
+
]);
|
|
11
|
+
const isUserFacingFailureReason = (value)=>REASONS.has(value);
|
|
12
|
+
/** `detail` is for logs and the job record; it is never shown to a user. */ export function markFailureReason(reason, detail) {
|
|
13
|
+
return `[${MARKER_PREFIX}${reason}] ${detail}`;
|
|
14
|
+
}
|
|
15
|
+
/** Anchored: a marked message quoted inside another error's text is not that error's own reason. */ export function readFailureReason(message) {
|
|
16
|
+
const match = MARKER.exec(message);
|
|
17
|
+
const reason = match?.[1];
|
|
18
|
+
return reason !== undefined && isUserFacingFailureReason(reason) ? reason : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=failureReason.js.map
|
|
@@ -116,8 +116,11 @@ const asObject = (value)=>isObject(value) ? value : {};
|
|
|
116
116
|
target: this.targetData,
|
|
117
117
|
path: []
|
|
118
118
|
}, walker);
|
|
119
|
+
// A detached copy, because write-back mutates an object-valued leaf (a rich-text tree) in
|
|
120
|
+
// place: seating the caller's own object here would translate their source document. Scalars
|
|
121
|
+
// are immutable, so they are seated as they are.
|
|
119
122
|
for (const { dataRef, key, sourceValue } of selected){
|
|
120
|
-
dataRef[key] = sourceValue;
|
|
123
|
+
dataRef[key] = isObject(sourceValue) ? structuredClone(sourceValue) : sourceValue;
|
|
121
124
|
}
|
|
122
125
|
return chunks;
|
|
123
126
|
}
|
|
@@ -25,9 +25,12 @@ export declare class ProvenanceService {
|
|
|
25
25
|
private readonly schemaMap;
|
|
26
26
|
constructor(payload: Payload, store: ProvenanceStore, schemaMap: CollectionSchemaMap);
|
|
27
27
|
/**
|
|
28
|
-
* Hash the
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* Hash the source the translation was made from — the baseline staleness is later measured against.
|
|
29
|
+
*
|
|
30
|
+
* Ordering used to matter: the pipeline wrote into object-valued leaves it shared with the caller's
|
|
31
|
+
* source, so hashing afterwards captured the translation and reported every fresh translation as
|
|
32
|
+
* stale. It now detaches those leaves, so this may be called on either side of the pipeline.
|
|
33
|
+
*
|
|
31
34
|
* Returns `null` on any failure (no schema, hashing error) so provenance is skipped, not the translation.
|
|
32
35
|
*/
|
|
33
36
|
captureFingerprint(collection: CollectionSlug, sourceData: Record<string, unknown>): string | null;
|
|
@@ -19,9 +19,12 @@ import { fetchSourceDocument } from "../../shared/payload/sourceDocument";
|
|
|
19
19
|
this.schemaMap = schemaMap;
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
* Hash the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* Hash the source the translation was made from — the baseline staleness is later measured against.
|
|
23
|
+
*
|
|
24
|
+
* Ordering used to matter: the pipeline wrote into object-valued leaves it shared with the caller's
|
|
25
|
+
* source, so hashing afterwards captured the translation and reported every fresh translation as
|
|
26
|
+
* stale. It now detaches those leaves, so this may be called on either side of the pipeline.
|
|
27
|
+
*
|
|
25
28
|
* Returns `null` on any failure (no schema, hashing error) so provenance is skipped, not the translation.
|
|
26
29
|
*/ captureFingerprint(collection, sourceData) {
|
|
27
30
|
const schema = this.schemaMap.get(collection);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { ServerResponse } from "./ServerResponse";
|
|
2
2
|
export { withErrorHandler } from "./withErrorHandler";
|
|
3
3
|
export { withAccessCheck } from "./withAccessCheck";
|
|
4
|
-
export { toClientErrorMessage, GENERIC_TRANSLATION_ERROR } from "./toClientErrorMessage";
|
|
4
|
+
export { toClientErrorMessage, failureReasonText, GENERIC_TRANSLATION_ERROR, } from "./toClientErrorMessage";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { ServerResponse } from "./ServerResponse";
|
|
2
2
|
export { withErrorHandler } from "./withErrorHandler";
|
|
3
3
|
export { withAccessCheck } from "./withAccessCheck";
|
|
4
|
-
export { toClientErrorMessage, GENERIC_TRANSLATION_ERROR } from "./toClientErrorMessage";
|
|
4
|
+
export { toClientErrorMessage, failureReasonText, GENERIC_TRANSLATION_ERROR } from "./toClientErrorMessage";
|
|
5
5
|
|
|
6
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Call this on any message before it reaches a user — see {@link REASON_TEXT}. */
|
|
2
|
+
export declare function failureReasonText(message?: string): string | null;
|
|
1
3
|
/** Shown to the browser instead of a raw provider/runtime error outside development. */
|
|
2
4
|
export declare const GENERIC_TRANSLATION_ERROR = "Translation failed. See the server logs for details.";
|
|
3
5
|
/**
|
|
@@ -5,11 +7,12 @@ export declare const GENERIC_TRANSLATION_ERROR = "Translation failed. See the se
|
|
|
5
7
|
*
|
|
6
8
|
* Provider/runtime errors (e.g. `401 Incorrect API key provided: sk-proj-…`) leak implementation
|
|
7
9
|
* detail — and sometimes secrets — so the client must not see them in production. The full error
|
|
8
|
-
* still lives in the job record and server logs for debugging
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* still lives in the job record and server logs for debugging. Fail-safe: outside
|
|
11
|
+
* `development`/`test` (including an unset `NODE_ENV`) the raw message never leaves this function.
|
|
12
|
+
* A message carrying a failure reason is answered from this file's own catalogue instead, in every
|
|
13
|
+
* environment, and its tail is never shown.
|
|
11
14
|
*
|
|
12
15
|
* @param message - The raw server-side error message, if any.
|
|
13
|
-
* @returns The raw message in a debug environment, otherwise the generic text.
|
|
16
|
+
* @returns The raw message in a debug environment, otherwise the generic text or a reason's copy.
|
|
14
17
|
*/
|
|
15
18
|
export declare function toClientErrorMessage(message?: string): string;
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { readFailureReason } from "../../../core/domain/translation-providers/failureReason";
|
|
2
|
+
/** Re-authored here, never taken from the error: only text this file owns is free of vendor detail. */ const REASON_TEXT = {
|
|
3
|
+
"model-unavailable": "The configured translation model is not available to this API key. Set `model` in the provider configuration to one your key can use."
|
|
4
|
+
};
|
|
5
|
+
/** Call this on any message before it reaches a user — see {@link REASON_TEXT}. */ export function failureReasonText(message) {
|
|
6
|
+
const reason = message === undefined ? null : readFailureReason(message);
|
|
7
|
+
return reason === null ? null : REASON_TEXT[reason];
|
|
8
|
+
}
|
|
1
9
|
/** Shown to the browser instead of a raw provider/runtime error outside development. */ export const GENERIC_TRANSLATION_ERROR = "Translation failed. See the server logs for details.";
|
|
2
10
|
// Only these environments get the raw message as a debug aid. Anything else — production, an unset
|
|
3
11
|
// or misconfigured NODE_ENV — is treated as "not debug", so the default is the safe, generic text.
|
|
@@ -10,13 +18,16 @@ const DEBUG_ENVS = new Set([
|
|
|
10
18
|
*
|
|
11
19
|
* Provider/runtime errors (e.g. `401 Incorrect API key provided: sk-proj-…`) leak implementation
|
|
12
20
|
* detail — and sometimes secrets — so the client must not see them in production. The full error
|
|
13
|
-
* still lives in the job record and server logs for debugging
|
|
14
|
-
*
|
|
15
|
-
*
|
|
21
|
+
* still lives in the job record and server logs for debugging. Fail-safe: outside
|
|
22
|
+
* `development`/`test` (including an unset `NODE_ENV`) the raw message never leaves this function.
|
|
23
|
+
* A message carrying a failure reason is answered from this file's own catalogue instead, in every
|
|
24
|
+
* environment, and its tail is never shown.
|
|
16
25
|
*
|
|
17
26
|
* @param message - The raw server-side error message, if any.
|
|
18
|
-
* @returns The raw message in a debug environment, otherwise the generic text.
|
|
27
|
+
* @returns The raw message in a debug environment, otherwise the generic text or a reason's copy.
|
|
19
28
|
*/ export function toClientErrorMessage(message) {
|
|
29
|
+
const reasonText = failureReasonText(message);
|
|
30
|
+
if (reasonText !== null) return reasonText;
|
|
20
31
|
if (DEBUG_ENVS.has(process.env.NODE_ENV ?? "")) {
|
|
21
32
|
return message?.trim() || GENERIC_TRANSLATION_ERROR;
|
|
22
33
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { APIError } from "payload";
|
|
2
2
|
import { ServerResponse } from "./ServerResponse";
|
|
3
|
+
import { failureReasonText } from "./toClientErrorMessage";
|
|
3
4
|
/**
|
|
4
5
|
* Wraps async handler with error handling.
|
|
5
6
|
* Catches APIError and generic Error, returns appropriate HTTP response.
|
|
@@ -13,7 +14,7 @@ import { ServerResponse } from "./ServerResponse";
|
|
|
13
14
|
return ServerResponse.custom(e.message, e.status);
|
|
14
15
|
}
|
|
15
16
|
if (e instanceof Error) {
|
|
16
|
-
return ServerResponse.internalServerError(e.message);
|
|
17
|
+
return ServerResponse.internalServerError(failureReasonText(e.message) ?? e.message);
|
|
17
18
|
}
|
|
18
19
|
return ServerResponse.internalServerError();
|
|
19
20
|
}
|
|
@@ -6,7 +6,7 @@ type OpenAIProviderBase = {
|
|
|
6
6
|
/**
|
|
7
7
|
* Model used for translation.
|
|
8
8
|
*
|
|
9
|
-
* @default 'gpt-
|
|
9
|
+
* @default 'gpt-5.4-mini' — may move in a minor release; pin it if you need reproducibility.
|
|
10
10
|
*/
|
|
11
11
|
model?: string;
|
|
12
12
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createTranslationProvider } from "../shared";
|
|
2
2
|
import { loadOpenAIClient } from "./loadOpenAIClient";
|
|
3
3
|
import { openAIComplete } from "./openAIComplete";
|
|
4
|
-
const DEFAULT_MODEL = "gpt-
|
|
4
|
+
const DEFAULT_MODEL = "gpt-5.4-mini";
|
|
5
5
|
/** The SDK's own default is ten minutes — far too long when a translation blocks a live editor request. */ const DEFAULT_TIMEOUT_MS = 60_000;
|
|
6
6
|
/**
|
|
7
7
|
* Creates an OpenAI translation provider.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { markFailureReason } from "../../core/domain/translation-providers/failureReason";
|
|
2
|
+
import { isObject } from "../../core/kernel/utils/isObject";
|
|
1
3
|
import { errorMessageLower, NoContentError, ProviderConfigurationError } from "../shared";
|
|
2
4
|
const SCHEMA_NAME = "translation";
|
|
3
5
|
/**
|
|
@@ -16,6 +18,17 @@ const SCHEMA_NAME = "translation";
|
|
|
16
18
|
if (text.includes("invalid schema")) return "schema-rejected";
|
|
17
19
|
return null;
|
|
18
20
|
}
|
|
21
|
+
const MODEL_ACCESS_CODE = "model_not_found";
|
|
22
|
+
/**
|
|
23
|
+
* Only for a gateway that forwards the text and no `code` — Azure, OpenRouter, a corporate proxy.
|
|
24
|
+
* A full phrase rather than the word `model`, which such a gateway also echoes in rate limits.
|
|
25
|
+
* Wording as observed 2026-09; the code above is what carries this when OpenAI rewords it.
|
|
26
|
+
*/ const MODEL_ACCESS_PHRASE = "does not exist or you do not have access";
|
|
27
|
+
function isModelAccessFailure(cause) {
|
|
28
|
+
if (isObject(cause) && cause.code === MODEL_ACCESS_CODE) return true;
|
|
29
|
+
const text = errorMessageLower(cause);
|
|
30
|
+
return text !== null && text.includes(MODEL_ACCESS_PHRASE);
|
|
31
|
+
}
|
|
19
32
|
/**
|
|
20
33
|
* The vendor boundary: the only place OpenAI's response shape is read.
|
|
21
34
|
*
|
|
@@ -60,6 +73,11 @@ const SCHEMA_NAME = "translation";
|
|
|
60
73
|
signal
|
|
61
74
|
});
|
|
62
75
|
} catch (cause) {
|
|
76
|
+
if (isModelAccessFailure(cause)) {
|
|
77
|
+
throw new ProviderConfigurationError(markFailureReason("model-unavailable", `The model "${model}" is not available to this API key. Pass \`model\` in the provider configuration to pin one your key can use.`), {
|
|
78
|
+
cause
|
|
79
|
+
});
|
|
80
|
+
}
|
|
63
81
|
const rejection = structuredOutput === "json_schema" ? classifySchemaRejection(cause) : null;
|
|
64
82
|
if (rejection === "model-does-not-support") {
|
|
65
83
|
throw new ProviderConfigurationError(`The model "${model}" does not support the json_schema response format. Pass structuredOutput: "json_object", or choose a model that supports structured outputs.`, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@focus-reactive/payload-plugin-translator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Translation plugin for Payload CMS 3.x. Automatically translate your localized content using any translation provider.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|