@focus-reactive/payload-plugin-translator 0.10.4 → 0.11.1
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 +199 -11
- package/dist/client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.server.js +2 -2
- package/dist/client/widgets/translate-document/ui/TranslateDocument.server.js +2 -2
- package/dist/client/widgets/translate-field-control/ui/TranslateFieldControl.js +10 -7
- package/dist/core/translation-pipeline/stages/field-collector/FieldChunkCollector.js +0 -5
- package/dist/index.d.ts +8 -3
- package/dist/index.js +4 -3
- package/dist/server/features/translate-document/handler.d.ts +1 -0
- package/dist/server/features/translate-document/handler.js +55 -46
- package/dist/server/features/translate-document/targetLayer.d.ts +37 -0
- package/dist/server/features/translate-document/targetLayer.js +26 -0
- package/dist/server/features/translate-field/handler.js +4 -10
- package/dist/server/modules/auto-translate/AutoTranslateEnqueue.hook.js +2 -1
- package/dist/server/shared/guards/index.d.ts +0 -1
- package/dist/server/shared/guards/index.js +0 -1
- package/dist/server/shared/payload/sourceDocument.d.ts +2 -4
- package/dist/server/shared/payload/sourceDocument.js +10 -5
- package/dist/translation-providers/index.d.ts +1 -0
- package/dist/translation-providers/index.js +1 -7
- package/dist/translation-providers/openai/OpenAI.shapes.d.ts +52 -0
- package/dist/translation-providers/openai/OpenAI.shapes.js +13 -0
- package/dist/translation-providers/openai/OpenAITranslation.provider.d.ts +51 -112
- package/dist/translation-providers/openai/OpenAITranslation.provider.js +49 -137
- package/dist/translation-providers/openai/OpenAITranslationLegacy.provider.d.ts +11 -0
- package/dist/translation-providers/openai/OpenAITranslationLegacy.provider.js +15 -0
- package/dist/translation-providers/openai/index.d.ts +6 -1
- package/dist/translation-providers/openai/index.js +3 -3
- package/dist/translation-providers/openai/loadOpenAIClient.d.ts +24 -0
- package/dist/translation-providers/openai/loadOpenAIClient.js +79 -0
- package/dist/translation-providers/openai/openAIComplete.d.ts +39 -0
- package/dist/translation-providers/openai/openAIComplete.js +84 -0
- package/dist/translation-providers/shared/CompletionProvider.provider.d.ts +65 -0
- package/dist/translation-providers/shared/CompletionProvider.provider.js +93 -0
- package/dist/translation-providers/shared/buildResponseSchema.d.ts +13 -0
- package/dist/translation-providers/shared/buildResponseSchema.js +22 -0
- package/dist/translation-providers/shared/buildSystemPrompt.d.ts +28 -0
- package/dist/translation-providers/shared/buildSystemPrompt.js +25 -0
- package/dist/translation-providers/shared/errors/KeySetMismatchError.d.ts +15 -0
- package/dist/translation-providers/shared/errors/KeySetMismatchError.js +26 -0
- package/dist/translation-providers/shared/errors/NoContentError.d.ts +9 -0
- package/dist/translation-providers/shared/errors/NoContentError.js +10 -0
- package/dist/translation-providers/shared/errors/ProviderConfigurationError.d.ts +10 -0
- package/dist/translation-providers/shared/errors/ProviderConfigurationError.js +11 -0
- package/dist/translation-providers/shared/errors/TranslationProviderError.d.ts +18 -0
- package/dist/translation-providers/shared/errors/TranslationProviderError.js +21 -0
- package/dist/translation-providers/shared/errors/TransportError.d.ts +10 -0
- package/dist/translation-providers/shared/errors/TransportError.js +11 -0
- package/dist/translation-providers/shared/errors/UnparseableReplyError.d.ts +9 -0
- package/dist/translation-providers/shared/errors/UnparseableReplyError.js +10 -0
- package/dist/translation-providers/shared/errors/errorMessageLower.d.ts +1 -0
- package/dist/translation-providers/shared/errors/errorMessageLower.js +8 -0
- package/dist/translation-providers/shared/errors/index.d.ts +9 -0
- package/dist/translation-providers/shared/errors/index.js +10 -0
- package/dist/translation-providers/shared/errors/wrapTransportError.d.ts +7 -0
- package/dist/translation-providers/shared/errors/wrapTransportError.js +14 -0
- package/dist/translation-providers/shared/index.d.ts +8 -0
- package/dist/translation-providers/shared/index.js +5 -0
- package/dist/translation-providers/shared/parseAndValidateReply.d.ts +13 -0
- package/dist/translation-providers/shared/parseAndValidateReply.js +46 -0
- package/dist/translation-providers/shared/runDryRun.d.ts +20 -0
- package/dist/translation-providers/shared/runDryRun.js +17 -0
- package/package.json +1 -1
- package/dist/server/shared/guards/collection-guards.d.ts +0 -6
- package/dist/server/shared/guards/collection-guards.js +0 -16
|
@@ -2,6 +2,7 @@ import { getByPath, ServerResponse } from "../../shared";
|
|
|
2
2
|
import { translateContent } from "../../../core/translation-pipeline";
|
|
3
3
|
import { FieldTranslationInputSchema, MAX_FIELD_VALUE_BYTES } from "./model";
|
|
4
4
|
import { resolveFieldSubtree } from "./resolveFieldSubtree";
|
|
5
|
+
import { fetchSourceDocument } from "../../shared/payload/sourceDocument";
|
|
5
6
|
const byteLength = (value)=>new TextEncoder().encode(JSON.stringify(value) ?? "").length;
|
|
6
7
|
const noop = (value, level, message)=>({
|
|
7
8
|
status: "noop",
|
|
@@ -31,16 +32,9 @@ const noop = (value, level, message)=>({
|
|
|
31
32
|
const { collection_slug, field_path, target_lng, source_lng, doc_id } = parsed.data;
|
|
32
33
|
const fields = this.config.schemaMap.get(collection_slug);
|
|
33
34
|
if (!fields) return ServerResponse.badRequest(`Collection "${collection_slug}" is not available for translation`);
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
const sourceDoc = await req.payload.findByID({
|
|
38
|
-
collection: collection_slug,
|
|
39
|
-
id: doc_id,
|
|
40
|
-
locale: source_lng,
|
|
41
|
-
fallbackLocale: false,
|
|
42
|
-
depth: 0
|
|
43
|
-
});
|
|
35
|
+
// The whole document, not just the field: the resolver needs it to disambiguate `blocks`, whose
|
|
36
|
+
// `blockType` lives in the data.
|
|
37
|
+
const sourceDoc = await fetchSourceDocument(req.payload, collection_slug, doc_id, source_lng);
|
|
44
38
|
const sourceValue = getByPath(sourceDoc, field_path);
|
|
45
39
|
// Guard the *translated* payload (held synchronously through the provider call), not the
|
|
46
40
|
// request body, which now carries no field value.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasDraftsEnabled } from "payload/shared";
|
|
1
2
|
import { hasSourceContentChanged } from "../../../core/domain/auto-translate";
|
|
2
3
|
import { AUTO_TRANSLATE_CUSTOM_KEY } from "../../../core/domain/auto-translate";
|
|
3
4
|
import { AUTO_TRANSLATE_SKIP_CONTEXT_KEY } from "../../../types/AutoTranslateContext";
|
|
@@ -31,7 +32,7 @@ import { buildAutoTranslateTasks, passesPublishGate } from "./AutoTranslate.poli
|
|
|
31
32
|
return doc;
|
|
32
33
|
}
|
|
33
34
|
if (req.locale !== sourceLocale) return doc;
|
|
34
|
-
const hasDrafts =
|
|
35
|
+
const hasDrafts = hasDraftsEnabled(collection);
|
|
35
36
|
if (!passesPublishGate(doc, hasDrafts)) return doc;
|
|
36
37
|
const schema = schemaMap.get(collection.slug);
|
|
37
38
|
if (schema && !hasSourceContentChanged(previousDoc, doc, schema)) return doc;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { CollectionSlug, Payload } from "payload";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* just translated) and, through {@link ProvenanceService}, by the staleness **read** path
|
|
6
|
-
* (re-fingerprints the live source), so the fingerprint baseline can never drift between the two.
|
|
3
|
+
* The single source read: what "translate from X" resolves to. Both translation write paths and
|
|
4
|
+
* the staleness recompute must go through here, or the fingerprints they compare drift apart.
|
|
7
5
|
*/
|
|
8
6
|
export declare function fetchSourceDocument(payload: Payload, collection: CollectionSlug, id: string, locale: string): Promise<import("payload").JsonObject & import("payload").TypeWithID>;
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* just translated) and, through {@link ProvenanceService}, by the staleness **read** path
|
|
5
|
-
* (re-fingerprints the live source), so the fingerprint baseline can never drift between the two.
|
|
2
|
+
* The single source read: what "translate from X" resolves to. Both translation write paths and
|
|
3
|
+
* the staleness recompute must go through here, or the fingerprints they compare drift apart.
|
|
6
4
|
*/ export function fetchSourceDocument(payload, collection, id, locale) {
|
|
7
5
|
return payload.findByID({
|
|
8
6
|
collection,
|
|
9
7
|
id,
|
|
10
8
|
locale,
|
|
11
|
-
depth: 0
|
|
9
|
+
depth: 0,
|
|
10
|
+
// The current version, as the editor sees it. A published-row read is empty whenever the source
|
|
11
|
+
// locale is unpublished — which is exactly what a publish scoped to the target locale leaves
|
|
12
|
+
// behind — and then every fresh translation fingerprints as stale.
|
|
13
|
+
draft: true,
|
|
14
|
+
// Payload's locale fallback resolves an empty source locale to the default locale's text, so
|
|
15
|
+
// "translate from fr" would translate English and fingerprint English as the French source.
|
|
16
|
+
fallbackLocale: false
|
|
12
17
|
});
|
|
13
18
|
}
|
|
14
19
|
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
// opt-in — each vendor pulls its own deps (e.g. `openai`). Kept OUT of the framework-agnostic core
|
|
3
|
-
// so the core stays dependency-free; the PORT they implement lives in the core and is re-exported
|
|
4
|
-
// here for convenience. Naming is explicit ("translation-providers") to leave room for other kinds
|
|
5
|
-
// of providers later without a collision.
|
|
6
|
-
// The port (contract) — source of truth is the core.
|
|
7
|
-
// Vendor implementations.
|
|
1
|
+
export * from "./shared";
|
|
8
2
|
export * from "./openai";
|
|
9
3
|
|
|
10
4
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type OpenAIChatMessage = {
|
|
2
|
+
role: "system" | "user";
|
|
3
|
+
content: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* `schema` is `Record<string, unknown>`, not `unknown`: a widened field is not assignable to the
|
|
7
|
+
* SDK's closed union and OpenAI.shapes.test.ts stops compiling.
|
|
8
|
+
*/
|
|
9
|
+
export type OpenAIResponseFormat = {
|
|
10
|
+
type: "json_object";
|
|
11
|
+
} | {
|
|
12
|
+
type: "json_schema";
|
|
13
|
+
json_schema: {
|
|
14
|
+
name: string;
|
|
15
|
+
strict?: boolean;
|
|
16
|
+
schema: Record<string, unknown>;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export type OpenAIChatParams = {
|
|
20
|
+
model: string;
|
|
21
|
+
messages: OpenAIChatMessage[];
|
|
22
|
+
response_format?: OpenAIResponseFormat;
|
|
23
|
+
temperature?: number;
|
|
24
|
+
top_p?: number;
|
|
25
|
+
frequency_penalty?: number;
|
|
26
|
+
presence_penalty?: number;
|
|
27
|
+
};
|
|
28
|
+
export type OpenAIChatResult = {
|
|
29
|
+
choices: Array<{
|
|
30
|
+
message?: {
|
|
31
|
+
content?: string | null;
|
|
32
|
+
};
|
|
33
|
+
}>;
|
|
34
|
+
};
|
|
35
|
+
export type OpenAIRequestOptions = {
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Any client with a `chat.completions.create` of this shape.
|
|
40
|
+
*
|
|
41
|
+
* Method syntax, not a property with a function type: property syntax is checked contravariantly
|
|
42
|
+
* under strictFunctionTypes and the real SDK's overloaded `create` stops being assignable.
|
|
43
|
+
*
|
|
44
|
+
* @since 0.11.0
|
|
45
|
+
*/
|
|
46
|
+
export type OpenAIClientShape = {
|
|
47
|
+
chat: {
|
|
48
|
+
completions: {
|
|
49
|
+
create(params: OpenAIChatParams, options?: OpenAIRequestOptions): Promise<OpenAIChatResult>;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Never import from `openai` here, in any position: an SDK type would enter this package's emitted
|
|
2
|
+
// declarations and force every consumer to install the SDK to type-check. Chat Completions rather
|
|
3
|
+
// than the Responses API — that is what Azure, OpenRouter and LiteLLM gateways implement.
|
|
4
|
+
/**
|
|
5
|
+
* Any client with a `chat.completions.create` of this shape.
|
|
6
|
+
*
|
|
7
|
+
* Method syntax, not a property with a function type: property syntax is checked contravariantly
|
|
8
|
+
* under strictFunctionTypes and the real SDK's overloaded `create` stops being assignable.
|
|
9
|
+
*
|
|
10
|
+
* @since 0.11.0
|
|
11
|
+
*/ export { };
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=OpenAI.shapes.js.map
|
|
@@ -1,145 +1,84 @@
|
|
|
1
|
-
import type { TranslationProvider
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*/
|
|
7
|
-
type DryRunTransformer = (text: string) => string | Promise<string>;
|
|
8
|
-
/**
|
|
9
|
-
* Configuration for dry run mode with custom transformer.
|
|
10
|
-
*/
|
|
11
|
-
export type DryRunConfig = {
|
|
12
|
-
/** Custom transformer function for text */
|
|
13
|
-
transform: DryRunTransformer;
|
|
14
|
-
/** Delay in milliseconds before returning mock translation (simulates API latency) */
|
|
15
|
-
timeout?: number;
|
|
16
|
-
};
|
|
17
|
-
/**
|
|
18
|
-
* Context passed to the system prompt builder function.
|
|
19
|
-
*/
|
|
20
|
-
export type SystemPromptContext = {
|
|
21
|
-
/** Source language code (e.g., 'en', 'de') */
|
|
22
|
-
sourceLang: string;
|
|
23
|
-
/** Target language code (e.g., 'fr', 'es') */
|
|
24
|
-
targetLang: string;
|
|
25
|
-
/** Default system prompt that can be extended or replaced */
|
|
26
|
-
defaultPrompt: string;
|
|
27
|
-
};
|
|
28
|
-
/**
|
|
29
|
-
* Function to build a custom system prompt for translation.
|
|
30
|
-
*/
|
|
31
|
-
type SystemPromptBuilder = (context: SystemPromptContext) => string;
|
|
32
|
-
export type OpenAIProviderConfig = {
|
|
33
|
-
/** OpenAI API key (required). Read it from an env var — never hard-code it. */
|
|
34
|
-
apiKey: string;
|
|
1
|
+
import type { TranslationProvider } from "../../core/domain/translation-providers";
|
|
2
|
+
import type { DryRunConfig, SystemPromptBuilder } from "../shared";
|
|
3
|
+
import type { OpenAIClientShape } from "./OpenAI.shapes";
|
|
4
|
+
import type { OpenAISamplingParams, OpenAIStructuredOutput } from "./openAIComplete";
|
|
5
|
+
type OpenAIProviderBase = {
|
|
35
6
|
/**
|
|
36
|
-
*
|
|
7
|
+
* Model used for translation.
|
|
37
8
|
*
|
|
38
|
-
* @default 'gpt-4o'
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* model: 'gpt-4o-mini'
|
|
9
|
+
* @default 'gpt-4o' — may move in a minor release; pin it if you need reproducibility.
|
|
42
10
|
*/
|
|
43
|
-
model?:
|
|
11
|
+
model?: string;
|
|
44
12
|
/**
|
|
45
|
-
* Custom system
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* @example
|
|
49
|
-
* // Add custom instructions
|
|
50
|
-
* systemPrompt: ({ sourceLang, targetLang, defaultPrompt }) =>
|
|
51
|
-
* `${defaultPrompt}\nUse formal language. Keep brand names unchanged.`
|
|
52
|
-
*
|
|
53
|
-
* @example
|
|
54
|
-
* // Completely custom prompt
|
|
55
|
-
* systemPrompt: ({ sourceLang, targetLang }) =>
|
|
56
|
-
* `Translate JSON values from ${sourceLang} to ${targetLang}. Be concise.`
|
|
13
|
+
* Custom system-prompt builder. Receives the source and target languages plus the prompt this
|
|
14
|
+
* package would otherwise send, so you can extend it rather than rewrite it.
|
|
57
15
|
*/
|
|
58
16
|
systemPrompt?: SystemPromptBuilder;
|
|
59
17
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* - `true` — uses default transformer that reverses the text
|
|
63
|
-
* - `{ transform, timeout? }` — custom transformer with optional delay
|
|
64
|
-
*
|
|
65
|
-
* @example
|
|
66
|
-
* // Default behavior (reverse text, no delay)
|
|
67
|
-
* dryRun: true
|
|
68
|
-
*
|
|
69
|
-
* @example
|
|
70
|
-
* // Custom transformer with delay
|
|
71
|
-
* dryRun: {
|
|
72
|
-
* transform: (text) => `[TRANSLATED] ${text}`,
|
|
73
|
-
* timeout: 1000, // 1 second delay
|
|
74
|
-
* }
|
|
18
|
+
* Simulate translations without calling OpenAI — see {@link DryRunConfig}.
|
|
75
19
|
*
|
|
76
20
|
* @default false
|
|
21
|
+
* @deprecated Pass your own `client`, or build a provider with `createTranslationProvider({
|
|
22
|
+
* complete })`. Remove in next major. See docs/DEPRECATIONS.md#provider-dry-run
|
|
77
23
|
*/
|
|
78
24
|
dryRun?: boolean | DryRunConfig;
|
|
79
25
|
/**
|
|
80
|
-
* Per-request timeout in milliseconds for the
|
|
81
|
-
* call, so the OpenAI SDK default (10 minutes) is usually too long. Omit to keep the SDK default.
|
|
26
|
+
* Per-request timeout in milliseconds for the client this package builds.
|
|
82
27
|
*
|
|
83
|
-
*
|
|
84
|
-
* timeout: 60_000 // 60s
|
|
28
|
+
* Ignored when you pass your own `client` — then the timeout is whatever you configured on it.
|
|
85
29
|
*
|
|
30
|
+
* @default 60000
|
|
86
31
|
* @since 0.6.0
|
|
87
32
|
*/
|
|
88
33
|
timeout?: number;
|
|
89
34
|
/**
|
|
90
|
-
* Maximum automatic retries
|
|
91
|
-
*
|
|
35
|
+
* Maximum automatic retries on transient errors (429, 5xx, network) for the client this package
|
|
36
|
+
* builds. Ignored when you pass your own `client`.
|
|
92
37
|
*
|
|
38
|
+
* @default the SDK's own default (2)
|
|
93
39
|
* @since 0.6.0
|
|
94
40
|
*/
|
|
95
41
|
maxRetries?: number;
|
|
96
|
-
};
|
|
97
|
-
/** @deprecated Use `createOpenAIProvider` function instead */
|
|
98
|
-
export declare class OpenAITranslationProvider implements TranslationProvider {
|
|
99
|
-
private openAiClient;
|
|
100
|
-
private readonly config;
|
|
101
|
-
constructor(config: OpenAIProviderConfig);
|
|
102
|
-
translate(content: TranslationInput, souceLng: string, targetLng: string): Promise<TranslationOutput | null>;
|
|
103
42
|
/**
|
|
104
|
-
*
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Returns the transformer function for dry run mode.
|
|
109
|
-
* If dryRun is an object with transform, returns it.
|
|
110
|
-
* If dryRun is true, returns the default transformer that reverses text.
|
|
43
|
+
* Sampling parameters — see {@link OpenAISamplingParams}, which survives this option.
|
|
44
|
+
*
|
|
45
|
+
* @since 0.11.0
|
|
111
46
|
*/
|
|
112
|
-
|
|
47
|
+
sampling?: OpenAISamplingParams;
|
|
113
48
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
49
|
+
* Which structured-output envelope to send — see {@link OpenAIStructuredOutput}, which documents
|
|
50
|
+
* the trade-off and survives this option.
|
|
51
|
+
*
|
|
52
|
+
* @since 0.11.0
|
|
117
53
|
*/
|
|
118
|
-
|
|
119
|
-
|
|
54
|
+
structuredOutput?: OpenAIStructuredOutput;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Configuration for {@link createOpenAIProvider}: an API key **or** a ready-made client, never both.
|
|
58
|
+
*
|
|
59
|
+
* @deprecated Construct the client yourself and pass it to `openAIComplete`, which stays.
|
|
60
|
+
* Remove in next major. See docs/DEPRECATIONS.md#openai-client-construction
|
|
61
|
+
*/
|
|
62
|
+
export type OpenAIProviderConfig = OpenAIProviderBase & ({
|
|
63
|
+
apiKey: string;
|
|
64
|
+
client?: never;
|
|
65
|
+
} | {
|
|
120
66
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* @
|
|
67
|
+
* A ready-made client — the OpenAI SDK client, Azure, OpenRouter, a proxy. On this path the
|
|
68
|
+
* `openai` package is never loaded and `timeout` / `maxRetries` are yours, not ours.
|
|
69
|
+
*
|
|
70
|
+
* @since 0.11.0
|
|
125
71
|
*/
|
|
126
|
-
|
|
127
|
-
|
|
72
|
+
client: OpenAIClientShape;
|
|
73
|
+
apiKey?: never;
|
|
74
|
+
});
|
|
128
75
|
/**
|
|
129
76
|
* Creates an OpenAI translation provider.
|
|
130
77
|
*
|
|
131
|
-
* @
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* // With options
|
|
137
|
-
* createOpenAIProvider({
|
|
138
|
-
* apiKey: process.env.OPENAI_API_KEY,
|
|
139
|
-
* model: 'gpt-4o-mini',
|
|
140
|
-
* systemPrompt: ({ defaultPrompt }) => `${defaultPrompt}\nUse formal language.`,
|
|
141
|
-
* })
|
|
142
|
-
* ```
|
|
78
|
+
* @deprecated What this adds over `openAIComplete` is building the SDK client for you, and
|
|
79
|
+
* carrying `openai` as an optional dependency to do it. Construct the client yourself instead:
|
|
80
|
+
* `createTranslationProvider({ complete: openAIComplete({ client, model }) })`. Remove in next
|
|
81
|
+
* major. See the recipe in the README and docs/DEPRECATIONS.md#openai-client-construction
|
|
143
82
|
*/
|
|
144
|
-
export declare function createOpenAIProvider(config: OpenAIProviderConfig):
|
|
83
|
+
export declare function createOpenAIProvider(config: OpenAIProviderConfig): TranslationProvider;
|
|
145
84
|
export {};
|
|
@@ -1,144 +1,56 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
constructor(config){
|
|
7
|
-
// `timeout`/`maxRetries` are passed through to the OpenAI SDK; `undefined` keeps the SDK
|
|
8
|
-
// defaults (10 min timeout, 2 retries). A blocking translation job rarely wants the full 10 min.
|
|
9
|
-
this.openAiClient = new OpenAI({
|
|
10
|
-
apiKey: config.apiKey,
|
|
11
|
-
timeout: config.timeout,
|
|
12
|
-
maxRetries: config.maxRetries
|
|
13
|
-
});
|
|
14
|
-
this.config = config;
|
|
15
|
-
}
|
|
16
|
-
async translate(content, souceLng, targetLng) {
|
|
17
|
-
if (this.config.dryRun) {
|
|
18
|
-
console.info("[DRY RUN] Translation simulation:", {
|
|
19
|
-
content,
|
|
20
|
-
sourceLang: souceLng,
|
|
21
|
-
targetLang: targetLng,
|
|
22
|
-
provider: "OpenAI"
|
|
23
|
-
});
|
|
24
|
-
const timeout = this.getDryRunTimeout();
|
|
25
|
-
if (timeout > 0) await new Promise((resolve)=>setTimeout(resolve, timeout));
|
|
26
|
-
const transformer = this.getDryRunTransformer();
|
|
27
|
-
return this.createMockTranslation(content, transformer);
|
|
28
|
-
}
|
|
29
|
-
const systemPrompt = this.buildSystemPrompt(souceLng, targetLng);
|
|
30
|
-
const chatCompletion = await this.openAiClient.chat.completions.create({
|
|
31
|
-
messages: [
|
|
32
|
-
{
|
|
33
|
-
role: "system",
|
|
34
|
-
content: systemPrompt
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
role: "user",
|
|
38
|
-
content: JSON.stringify(content)
|
|
39
|
-
}
|
|
40
|
-
],
|
|
41
|
-
model: this.config.model ?? "gpt-4o",
|
|
42
|
-
temperature: 0,
|
|
43
|
-
top_p: 1,
|
|
44
|
-
frequency_penalty: 0,
|
|
45
|
-
presence_penalty: 0,
|
|
46
|
-
response_format: {
|
|
47
|
-
type: "json_object"
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
// Guard `choices[0]`: an empty `choices` array (e.g. content-filtered response) would otherwise
|
|
51
|
-
// throw a TypeError instead of the intended graceful `null`.
|
|
52
|
-
const translatedContent = chatCompletion.choices[0]?.message?.content;
|
|
53
|
-
if (!translatedContent) return null;
|
|
54
|
-
try {
|
|
55
|
-
return JSON.parse(translatedContent);
|
|
56
|
-
} catch {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Builds the system prompt for translation.
|
|
62
|
-
*/ buildSystemPrompt(sourceLang, targetLang) {
|
|
63
|
-
const defaultPrompt = `Translate the values from the JSON that the user will send you${sourceLang ? ` from ${sourceLang}` : ""} into ${targetLang}. Keep all JSON keys exactly as they are, only translate the values.
|
|
64
|
-
The response should be a valid JSON object with the same structure and keys as the input, but with translated values.
|
|
65
|
-
Maintain any special formatting, placeholders, or variables within the values if they exist.`;
|
|
66
|
-
if (this.config.systemPrompt) {
|
|
67
|
-
return this.config.systemPrompt({
|
|
68
|
-
sourceLang,
|
|
69
|
-
targetLang,
|
|
70
|
-
defaultPrompt
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
return defaultPrompt;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Returns the transformer function for dry run mode.
|
|
77
|
-
* If dryRun is an object with transform, returns it.
|
|
78
|
-
* If dryRun is true, returns the default transformer that reverses text.
|
|
79
|
-
*/ getDryRunTransformer() {
|
|
80
|
-
if (typeof this.config.dryRun === "object" && this.config.dryRun.transform) {
|
|
81
|
-
return this.config.dryRun.transform;
|
|
82
|
-
}
|
|
83
|
-
return (text)=>text.split("").reverse().join("");
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Returns the timeout for dry run mode.
|
|
87
|
-
* If dryRun is an object with timeout, returns it.
|
|
88
|
-
* Otherwise returns 0 (no delay).
|
|
89
|
-
*/ getDryRunTimeout() {
|
|
90
|
-
if (typeof this.config.dryRun === "object" && this.config.dryRun.timeout) {
|
|
91
|
-
return this.config.dryRun.timeout;
|
|
92
|
-
}
|
|
93
|
-
return 0;
|
|
94
|
-
}
|
|
95
|
-
async createMockTranslation(content, transformer) {
|
|
96
|
-
try {
|
|
97
|
-
const mockTranslation = await this.transformObjectValues(content, async (value)=>{
|
|
98
|
-
if (typeof value === "string" && value.trim()) return transformer(value);
|
|
99
|
-
return value;
|
|
100
|
-
});
|
|
101
|
-
return mockTranslation;
|
|
102
|
-
} catch {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Recursively transforms the values of an object or array using the provided transformer function.
|
|
108
|
-
* @param obj The object or value to transform.
|
|
109
|
-
* @param transformer The function to apply to each value.
|
|
110
|
-
* @returns The transformed object, array, or value.
|
|
111
|
-
*/ async transformObjectValues(obj, transformer) {
|
|
112
|
-
if (Array.isArray(obj)) {
|
|
113
|
-
return Promise.all(obj.map((item)=>this.transformObjectValues(item, transformer)));
|
|
114
|
-
}
|
|
115
|
-
if (isObject(obj)) {
|
|
116
|
-
const result = {};
|
|
117
|
-
for (const [key, value] of Object.entries(obj)){
|
|
118
|
-
result[key] = await this.transformObjectValues(value, transformer);
|
|
119
|
-
}
|
|
120
|
-
return result;
|
|
121
|
-
}
|
|
122
|
-
return transformer(obj);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
1
|
+
import { createTranslationProvider } from "../shared";
|
|
2
|
+
import { loadOpenAIClient } from "./loadOpenAIClient";
|
|
3
|
+
import { openAIComplete } from "./openAIComplete";
|
|
4
|
+
const DEFAULT_MODEL = "gpt-4o";
|
|
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;
|
|
125
6
|
/**
|
|
126
7
|
* Creates an OpenAI translation provider.
|
|
127
8
|
*
|
|
128
|
-
* @
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* // With options
|
|
134
|
-
* createOpenAIProvider({
|
|
135
|
-
* apiKey: process.env.OPENAI_API_KEY,
|
|
136
|
-
* model: 'gpt-4o-mini',
|
|
137
|
-
* systemPrompt: ({ defaultPrompt }) => `${defaultPrompt}\nUse formal language.`,
|
|
138
|
-
* })
|
|
139
|
-
* ```
|
|
9
|
+
* @deprecated What this adds over `openAIComplete` is building the SDK client for you, and
|
|
10
|
+
* carrying `openai` as an optional dependency to do it. Construct the client yourself instead:
|
|
11
|
+
* `createTranslationProvider({ complete: openAIComplete({ client, model }) })`. Remove in next
|
|
12
|
+
* major. See the recipe in the README and docs/DEPRECATIONS.md#openai-client-construction
|
|
140
13
|
*/ export function createOpenAIProvider(config) {
|
|
141
|
-
|
|
14
|
+
const { model = DEFAULT_MODEL, systemPrompt, dryRun, sampling, structuredOutput } = config;
|
|
15
|
+
// The promise, not the client: two concurrent first calls would otherwise each start their own
|
|
16
|
+
// import. Per instance, never module-level — one consumer's client must not reach a differently
|
|
17
|
+
// configured provider in the same process.
|
|
18
|
+
let clientPromise;
|
|
19
|
+
const loadClientAndForgetOnFailure = async ()=>{
|
|
20
|
+
try {
|
|
21
|
+
// `apiKey` goes through unchanged — never defaulted to "". The SDK checks for `undefined`, so an
|
|
22
|
+
// empty string would build a client that 401s on every request instead of saying the key is
|
|
23
|
+
// missing.
|
|
24
|
+
return await loadOpenAIClient({
|
|
25
|
+
apiKey: config.apiKey,
|
|
26
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT_MS,
|
|
27
|
+
maxRetries: config.maxRetries
|
|
28
|
+
});
|
|
29
|
+
} catch (error) {
|
|
30
|
+
// A cached rejected promise makes one bad first call permanent — the job runner's retries
|
|
31
|
+
// would replay the same stale error.
|
|
32
|
+
clientPromise = undefined;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const resolveClient = ()=>{
|
|
37
|
+
if (config.client) return Promise.resolve(config.client);
|
|
38
|
+
clientPromise ??= loadClientAndForgetOnFailure();
|
|
39
|
+
return clientPromise;
|
|
40
|
+
};
|
|
41
|
+
return createTranslationProvider({
|
|
42
|
+
systemPrompt,
|
|
43
|
+
dryRun,
|
|
44
|
+
complete: async (request)=>{
|
|
45
|
+
const client = await resolveClient();
|
|
46
|
+
return openAIComplete({
|
|
47
|
+
client,
|
|
48
|
+
model,
|
|
49
|
+
sampling,
|
|
50
|
+
structuredOutput
|
|
51
|
+
})(request);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
142
54
|
}
|
|
143
55
|
|
|
144
56
|
//# sourceMappingURL=OpenAITranslation.provider.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TranslationInput, TranslationOutput, TranslationProvider } from "../../core/domain/translation-providers";
|
|
2
|
+
import type { OpenAIProviderConfig } from "./OpenAITranslation.provider";
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated Use {@link createOpenAIProvider} instead.
|
|
5
|
+
* See docs/DEPRECATIONS.md#openai-translation-provider-class
|
|
6
|
+
*/
|
|
7
|
+
export declare class OpenAITranslationProvider implements TranslationProvider {
|
|
8
|
+
private readonly inner;
|
|
9
|
+
constructor(config: OpenAIProviderConfig);
|
|
10
|
+
translate(input: TranslationInput, sourceLng: string, targetLng: string): Promise<TranslationOutput | null>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createOpenAIProvider } from "./OpenAITranslation.provider";
|
|
2
|
+
/**
|
|
3
|
+
* @deprecated Use {@link createOpenAIProvider} instead.
|
|
4
|
+
* See docs/DEPRECATIONS.md#openai-translation-provider-class
|
|
5
|
+
*/ export class OpenAITranslationProvider {
|
|
6
|
+
inner;
|
|
7
|
+
constructor(config){
|
|
8
|
+
this.inner = createOpenAIProvider(config);
|
|
9
|
+
}
|
|
10
|
+
translate(input, sourceLng, targetLng) {
|
|
11
|
+
return this.inner.translate(input, sourceLng, targetLng);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
//# sourceMappingURL=OpenAITranslationLegacy.provider.js.map
|
|
@@ -1 +1,6 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { createOpenAIProvider } from "./OpenAITranslation.provider";
|
|
2
|
+
export { OpenAITranslationProvider } from "./OpenAITranslationLegacy.provider";
|
|
3
|
+
export type { OpenAIProviderConfig } from "./OpenAITranslation.provider";
|
|
4
|
+
export type { OpenAIClientShape } from "./OpenAI.shapes";
|
|
5
|
+
export { openAIComplete } from "./openAIComplete";
|
|
6
|
+
export type { OpenAISamplingParams, OpenAIStructuredOutput } from "./openAIComplete";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export {
|
|
1
|
+
export { createOpenAIProvider } from "./OpenAITranslation.provider";
|
|
2
|
+
export { OpenAITranslationProvider } from "./OpenAITranslationLegacy.provider";
|
|
3
|
+
export { openAIComplete } from "./openAIComplete";
|
|
4
4
|
|
|
5
5
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { OpenAIClientShape } from "./OpenAI.shapes";
|
|
2
|
+
export type OpenAIClientOptions = {
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
timeout?: number;
|
|
5
|
+
maxRetries?: number;
|
|
6
|
+
};
|
|
7
|
+
type OpenAISdkModule = {
|
|
8
|
+
default: new (opts: OpenAIClientOptions) => unknown;
|
|
9
|
+
};
|
|
10
|
+
export type OpenAISdkImporter = () => Promise<OpenAISdkModule>;
|
|
11
|
+
/**
|
|
12
|
+
* Was `moduleSpecifier` itself missing, or one of its dependencies? Exported for its own test; not
|
|
13
|
+
* public API.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isModuleNotFound(cause: unknown, moduleSpecifier: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Constructs an OpenAI SDK client, importing the `openai` package on every call — callers memoize.
|
|
18
|
+
* Reached only when a consumer passed `apiKey`; a consumer who injects a `client` never runs this
|
|
19
|
+
* module.
|
|
20
|
+
*
|
|
21
|
+
* @throws ProviderConfigurationError for a missing package, a broken install, or rejected options.
|
|
22
|
+
*/
|
|
23
|
+
export declare function loadOpenAIClient(options: OpenAIClientOptions, importSdk?: OpenAISdkImporter): Promise<OpenAIClientShape>;
|
|
24
|
+
export {};
|