@focus-reactive/payload-plugin-translator 0.10.3 → 0.11.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.
Files changed (56) hide show
  1. package/README.md +152 -10
  2. package/dist/core/domain/locales/index.d.ts +6 -0
  3. package/dist/core/domain/locales/index.js +7 -0
  4. package/dist/core/domain/locales/resolveLocales.d.ts +59 -0
  5. package/dist/core/domain/locales/resolveLocales.js +68 -0
  6. package/dist/index.d.ts +8 -3
  7. package/dist/index.js +4 -3
  8. package/dist/server/features/enqueue-translation/handler.js +2 -2
  9. package/dist/server/modules/auto-translate/AutoTranslate.policy.js +7 -8
  10. package/dist/translation-providers/index.d.ts +1 -0
  11. package/dist/translation-providers/index.js +1 -7
  12. package/dist/translation-providers/openai/OpenAI.shapes.d.ts +52 -0
  13. package/dist/translation-providers/openai/OpenAI.shapes.js +13 -0
  14. package/dist/translation-providers/openai/OpenAITranslation.provider.d.ts +51 -112
  15. package/dist/translation-providers/openai/OpenAITranslation.provider.js +49 -137
  16. package/dist/translation-providers/openai/OpenAITranslationLegacy.provider.d.ts +11 -0
  17. package/dist/translation-providers/openai/OpenAITranslationLegacy.provider.js +15 -0
  18. package/dist/translation-providers/openai/index.d.ts +6 -1
  19. package/dist/translation-providers/openai/index.js +3 -3
  20. package/dist/translation-providers/openai/loadOpenAIClient.d.ts +24 -0
  21. package/dist/translation-providers/openai/loadOpenAIClient.js +79 -0
  22. package/dist/translation-providers/openai/openAIComplete.d.ts +39 -0
  23. package/dist/translation-providers/openai/openAIComplete.js +84 -0
  24. package/dist/translation-providers/shared/CompletionProvider.provider.d.ts +65 -0
  25. package/dist/translation-providers/shared/CompletionProvider.provider.js +93 -0
  26. package/dist/translation-providers/shared/buildResponseSchema.d.ts +13 -0
  27. package/dist/translation-providers/shared/buildResponseSchema.js +22 -0
  28. package/dist/translation-providers/shared/buildSystemPrompt.d.ts +28 -0
  29. package/dist/translation-providers/shared/buildSystemPrompt.js +25 -0
  30. package/dist/translation-providers/shared/errors/KeySetMismatchError.d.ts +15 -0
  31. package/dist/translation-providers/shared/errors/KeySetMismatchError.js +26 -0
  32. package/dist/translation-providers/shared/errors/NoContentError.d.ts +9 -0
  33. package/dist/translation-providers/shared/errors/NoContentError.js +10 -0
  34. package/dist/translation-providers/shared/errors/ProviderConfigurationError.d.ts +10 -0
  35. package/dist/translation-providers/shared/errors/ProviderConfigurationError.js +11 -0
  36. package/dist/translation-providers/shared/errors/TranslationProviderError.d.ts +18 -0
  37. package/dist/translation-providers/shared/errors/TranslationProviderError.js +21 -0
  38. package/dist/translation-providers/shared/errors/TransportError.d.ts +10 -0
  39. package/dist/translation-providers/shared/errors/TransportError.js +11 -0
  40. package/dist/translation-providers/shared/errors/UnparseableReplyError.d.ts +9 -0
  41. package/dist/translation-providers/shared/errors/UnparseableReplyError.js +10 -0
  42. package/dist/translation-providers/shared/errors/errorMessageLower.d.ts +1 -0
  43. package/dist/translation-providers/shared/errors/errorMessageLower.js +8 -0
  44. package/dist/translation-providers/shared/errors/index.d.ts +9 -0
  45. package/dist/translation-providers/shared/errors/index.js +10 -0
  46. package/dist/translation-providers/shared/errors/wrapTransportError.d.ts +7 -0
  47. package/dist/translation-providers/shared/errors/wrapTransportError.js +14 -0
  48. package/dist/translation-providers/shared/index.d.ts +8 -0
  49. package/dist/translation-providers/shared/index.js +5 -0
  50. package/dist/translation-providers/shared/parseAndValidateReply.d.ts +13 -0
  51. package/dist/translation-providers/shared/parseAndValidateReply.js +46 -0
  52. package/dist/translation-providers/shared/runDryRun.d.ts +20 -0
  53. package/dist/translation-providers/shared/runDryRun.js +17 -0
  54. package/package.json +1 -1
  55. package/dist/server/features/enqueue-translation/resolveTargetLocales.d.ts +0 -30
  56. package/dist/server/features/enqueue-translation/resolveTargetLocales.js +0 -35
@@ -1,145 +1,84 @@
1
- import type { TranslationProvider, TranslationInput, TranslationOutput } from "../../core/domain/translation-providers/TranslationProvider.interface";
2
- import type { ChatModel } from "openai/resources/index.mjs";
3
- /**
4
- * Function to transform text in dry run mode.
5
- * Receives the original text and returns the transformed text.
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
- * OpenAI model to use for translation.
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?: (string & {}) | ChatModel;
11
+ model?: string;
44
12
  /**
45
- * Custom system prompt builder for translation.
46
- * Receives context with source/target languages and the default prompt.
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
- * When enabled, simulates translations without making actual API calls to OpenAI.
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 OpenAI client. A translation job blocks on this
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
- * @example
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 the OpenAI client performs on transient errors (429, 5xx, network).
91
- * Omit to keep the SDK default (2). Set `0` to disable retries.
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
- * Builds the system prompt for translation.
105
- */
106
- private buildSystemPrompt;
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
- private getDryRunTransformer;
47
+ sampling?: OpenAISamplingParams;
113
48
  /**
114
- * Returns the timeout for dry run mode.
115
- * If dryRun is an object with timeout, returns it.
116
- * Otherwise returns 0 (no delay).
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
- private getDryRunTimeout;
119
- private createMockTranslation;
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
- * Recursively transforms the values of an object or array using the provided transformer function.
122
- * @param obj The object or value to transform.
123
- * @param transformer The function to apply to each value.
124
- * @returns The transformed object, array, or value.
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
- private transformObjectValues;
127
- }
72
+ client: OpenAIClientShape;
73
+ apiKey?: never;
74
+ });
128
75
  /**
129
76
  * Creates an OpenAI translation provider.
130
77
  *
131
- * @example
132
- * ```ts
133
- * // Basic usage
134
- * createOpenAIProvider({ apiKey: process.env.OPENAI_API_KEY })
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): OpenAITranslationProvider;
83
+ export declare function createOpenAIProvider(config: OpenAIProviderConfig): TranslationProvider;
145
84
  export {};
@@ -1,144 +1,56 @@
1
- import OpenAI from "openai";
2
- import { isObject } from "../../core/kernel/utils/isObject";
3
- /** @deprecated Use `createOpenAIProvider` function instead */ export class OpenAITranslationProvider {
4
- openAiClient;
5
- config;
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
- * @example
129
- * ```ts
130
- * // Basic usage
131
- * createOpenAIProvider({ apiKey: process.env.OPENAI_API_KEY })
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
- return new OpenAITranslationProvider(config);
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 { OpenAITranslationProvider, createOpenAIProvider, type OpenAIProviderConfig, type DryRunConfig, type SystemPromptContext, } from "./OpenAITranslation.provider";
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
- // OpenAI translation provider one vendor implementation of the core `TranslationProvider`
2
- // port. Payload-free; pulls the `openai` SDK (opt-in).
3
- export { OpenAITranslationProvider, createOpenAIProvider } from "./OpenAITranslation.provider";
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 {};
@@ -0,0 +1,79 @@
1
+ import { errorMessageLower, ProviderConfigurationError } from "../shared";
2
+ /**
3
+ * A literal `import("openai")` is type-resolved like a static import and would put the SDK into the
4
+ * emitted .d.ts, forcing every consumer to install it. Through a constant it resolves at runtime only.
5
+ */ const OPENAI_MODULE = "openai";
6
+ const MODULE_NOT_FOUND_CODES = new Set([
7
+ "ERR_MODULE_NOT_FOUND",
8
+ "MODULE_NOT_FOUND"
9
+ ]);
10
+ /**
11
+ * Bundlers and test runners re-throw resolution failures in their own shapes, and several drop the
12
+ * `code` on the way. The wording is the more portable signal, so both are checked.
13
+ */ const MODULE_NOT_FOUND_MESSAGES = {
14
+ nodeCjs: "cannot find module",
15
+ nodeEsm: "cannot find package",
16
+ viteRollup: "failed to resolve",
17
+ viteDev: "failed to load url",
18
+ webpack: "module not found"
19
+ };
20
+ /**
21
+ * Each runtime's message names two things: what was not found and who imported it. The importer half
22
+ * is a path inside node_modules/openai, so it must be cut off before searching for "openai".
23
+ */ const IMPORTER_CLAUSES = {
24
+ nodeEsm: " imported from ",
25
+ nodeCjs: "require stack:",
26
+ vite: " from ",
27
+ webpack: " in "
28
+ };
29
+ /**
30
+ * `@vercel/nft` (Vercel builds, Next `output: "standalone"`) resolves `import()` statically and can
31
+ * follow a module-level constant but not a parameter — silently, so a parameterised specifier gets
32
+ * `openai` pruned from the deployment and the first production translation reports it missing.
33
+ */ async function importOpenAISdk() {
34
+ return await import(OPENAI_MODULE);
35
+ }
36
+ function whatCouldNotBeFound(text) {
37
+ return Object.values(IMPORTER_CLAUSES).reduce((remaining, clause)=>remaining.split(clause)[0], text);
38
+ }
39
+ /**
40
+ * Was `moduleSpecifier` itself missing, or one of its dependencies? Exported for its own test; not
41
+ * public API.
42
+ */ export function isModuleNotFound(cause, moduleSpecifier) {
43
+ const text = errorMessageLower(cause);
44
+ if (text === null) return false;
45
+ if (!whatCouldNotBeFound(text).includes(moduleSpecifier.toLowerCase())) return false;
46
+ const code = cause.code;
47
+ if (typeof code === "string" && MODULE_NOT_FOUND_CODES.has(code)) return true;
48
+ return Object.values(MODULE_NOT_FOUND_MESSAGES).some((phrase)=>text.includes(phrase));
49
+ }
50
+ /**
51
+ * Constructs an OpenAI SDK client, importing the `openai` package on every call — callers memoize.
52
+ * Reached only when a consumer passed `apiKey`; a consumer who injects a `client` never runs this
53
+ * module.
54
+ *
55
+ * @throws ProviderConfigurationError for a missing package, a broken install, or rejected options.
56
+ */ export async function loadOpenAIClient(options, importSdk = importOpenAISdk) {
57
+ let sdk;
58
+ try {
59
+ sdk = await importSdk();
60
+ } catch (cause) {
61
+ if (isModuleNotFound(cause, OPENAI_MODULE)) {
62
+ throw new ProviderConfigurationError("createOpenAIProvider({ apiKey }) needs the optional `openai` package. Install it, or pass a ready-made `client` instead.", {
63
+ cause
64
+ });
65
+ }
66
+ throw new ProviderConfigurationError("The `openai` package is installed but failed to load. See this error's `cause`.", {
67
+ cause
68
+ });
69
+ }
70
+ try {
71
+ return new sdk.default(options);
72
+ } catch (cause) {
73
+ throw new ProviderConfigurationError("The OpenAI client could not be constructed from the given options. See this error's `cause`.", {
74
+ cause
75
+ });
76
+ }
77
+ }
78
+
79
+ //# sourceMappingURL=loadOpenAIClient.js.map
@@ -0,0 +1,39 @@
1
+ import type { CompletionFn } from "../shared";
2
+ import type { OpenAIChatParams, OpenAIClientShape } from "./OpenAI.shapes";
3
+ /**
4
+ * Sampling parameters. Omitted from the request entirely unless set — several models reject them.
5
+ * Unset means the model's own defaults; pass `{ temperature: 0 }` for deterministic output.
6
+ *
7
+ * @since 0.11.0
8
+ */
9
+ export type OpenAISamplingParams = Pick<OpenAIChatParams, "temperature" | "top_p" | "frequency_penalty" | "presence_penalty">;
10
+ /**
11
+ * Which structured-output envelope to send.
12
+ *
13
+ * `json_schema` (the default): the reply must satisfy a schema, so a compliant model cannot drop a
14
+ * field. Older models and some gateways reject it with a 400, and the schema has a per-model
15
+ * property ceiling a large document can exceed.
16
+ *
17
+ * `json_object`: asks only for valid JSON. No ceiling, but a dropped key is then detected by
18
+ * key-set validation after the fact rather than prevented.
19
+ *
20
+ * The README weighs the two and explains how to size a document against the ceiling.
21
+ *
22
+ * @since 0.11.0
23
+ */
24
+ export type OpenAIStructuredOutput = "json_schema" | "json_object";
25
+ /**
26
+ * The vendor boundary: the only place OpenAI's response shape is read.
27
+ *
28
+ * Takes a client you constructed — it never loads the `openai` package — so pair it with
29
+ * `createTranslationProvider` to build a provider on an SDK version of your own choosing:
30
+ * `createTranslationProvider({ complete: openAIComplete({ client, model }) })`.
31
+ *
32
+ * @since 0.11.0
33
+ */
34
+ export declare function openAIComplete(args: {
35
+ client: OpenAIClientShape;
36
+ model: string;
37
+ sampling?: OpenAISamplingParams;
38
+ structuredOutput?: OpenAIStructuredOutput;
39
+ }): CompletionFn;
@@ -0,0 +1,84 @@
1
+ import { errorMessageLower, NoContentError, ProviderConfigurationError } from "../shared";
2
+ const SCHEMA_NAME = "translation";
3
+ /**
4
+ * Did the service reject the request over the schema envelope, and if so, why? The match stays
5
+ * narrow: a proxy that echoes the request body into an error string would otherwise turn every rate
6
+ * limit into a configuration problem.
7
+ */ function classifySchemaRejection(cause) {
8
+ const text = errorMessageLower(cause);
9
+ if (text === null) return null;
10
+ if (!text.includes("response_format") && !text.includes("json_schema")) return null;
11
+ // "…'response_format' of type 'json_schema' is not supported with this model."
12
+ if (text.includes("not supported") || text.includes("unsupported")) {
13
+ return "model-does-not-support";
14
+ }
15
+ // "Invalid schema for response_format 'translation': object has too many properties."
16
+ if (text.includes("invalid schema")) return "schema-rejected";
17
+ return null;
18
+ }
19
+ /**
20
+ * The vendor boundary: the only place OpenAI's response shape is read.
21
+ *
22
+ * Takes a client you constructed — it never loads the `openai` package — so pair it with
23
+ * `createTranslationProvider` to build a provider on an SDK version of your own choosing:
24
+ * `createTranslationProvider({ complete: openAIComplete({ client, model }) })`.
25
+ *
26
+ * @since 0.11.0
27
+ */ export function openAIComplete(args) {
28
+ const { client, model, sampling, structuredOutput = "json_schema" } = args;
29
+ return async ({ systemPrompt, userContent, responseSchema, signal })=>{
30
+ if (structuredOutput === "json_object" && !/json/iu.test(systemPrompt)) {
31
+ throw new ProviderConfigurationError('With structuredOutput: "json_object", OpenAI requires the word "json" somewhere in the prompt. Your systemPrompt override does not contain it.');
32
+ }
33
+ const params = {
34
+ model,
35
+ messages: [
36
+ {
37
+ role: "system",
38
+ content: systemPrompt
39
+ },
40
+ {
41
+ role: "user",
42
+ content: userContent
43
+ }
44
+ ],
45
+ response_format: structuredOutput === "json_schema" ? {
46
+ type: "json_schema",
47
+ json_schema: {
48
+ name: SCHEMA_NAME,
49
+ strict: true,
50
+ schema: responseSchema
51
+ }
52
+ } : {
53
+ type: "json_object"
54
+ },
55
+ ...sampling
56
+ };
57
+ let result;
58
+ try {
59
+ result = await client.chat.completions.create(params, {
60
+ signal
61
+ });
62
+ } catch (cause) {
63
+ const rejection = structuredOutput === "json_schema" ? classifySchemaRejection(cause) : null;
64
+ if (rejection === "model-does-not-support") {
65
+ 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.`, {
66
+ cause
67
+ });
68
+ }
69
+ if (rejection === "schema-rejected") {
70
+ throw new ProviderConfigurationError(`OpenAI rejected the generated response schema. This usually means the document has more translatable fields than a strict schema allows. Pass structuredOutput: "json_object", or translate a smaller subtree.`, {
71
+ cause
72
+ });
73
+ }
74
+ throw cause;
75
+ }
76
+ const content = result.choices[0]?.message?.content;
77
+ if (!content) {
78
+ throw new NoContentError("OpenAI returned no content. The reply may have been filtered, or the model produced nothing.");
79
+ }
80
+ return content;
81
+ };
82
+ }
83
+
84
+ //# sourceMappingURL=openAIComplete.js.map