champollion 0.3.3

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 (170) hide show
  1. package/LICENSE +133 -0
  2. package/README.md +387 -0
  3. package/bin/cli.js +278 -0
  4. package/index.js +135 -0
  5. package/lib/api-key.js +127 -0
  6. package/lib/autofix.js +432 -0
  7. package/lib/bridge/method_bridge.py +430 -0
  8. package/lib/card-source-resolution.mjs +284 -0
  9. package/lib/cards/cache.js +169 -0
  10. package/lib/cards/env.js +82 -0
  11. package/lib/cards/fetch-card-child.js +38 -0
  12. package/lib/cards/reader.js +435 -0
  13. package/lib/cards/refresh.js +111 -0
  14. package/lib/cards/remote.js +387 -0
  15. package/lib/cldf-export.mjs +540 -0
  16. package/lib/cldf-terms.mjs +62 -0
  17. package/lib/command-help.js +790 -0
  18. package/lib/commands/audit.js +49 -0
  19. package/lib/commands/card.js +454 -0
  20. package/lib/commands/doctor.js +559 -0
  21. package/lib/commands/fonts.js +489 -0
  22. package/lib/commands/help.js +91 -0
  23. package/lib/commands/init.js +1259 -0
  24. package/lib/commands/integrity.js +148 -0
  25. package/lib/commands/leaderboard.js +478 -0
  26. package/lib/commands/lint.js +30 -0
  27. package/lib/commands/models.js +177 -0
  28. package/lib/commands/plugin.js +103 -0
  29. package/lib/commands/provenance.js +45 -0
  30. package/lib/commands/recommend.js +75 -0
  31. package/lib/commands/register-corpus.js +678 -0
  32. package/lib/commands/repair-script.js +42 -0
  33. package/lib/commands/seal-corpus.js +355 -0
  34. package/lib/commands/seo.js +72 -0
  35. package/lib/commands/serve.js +147 -0
  36. package/lib/commands/status.js +265 -0
  37. package/lib/commands/submit.js +332 -0
  38. package/lib/commands/sync.js +89 -0
  39. package/lib/commands/tm.js +573 -0
  40. package/lib/commands/verify.js +39 -0
  41. package/lib/commands/watch.js +20 -0
  42. package/lib/commands/wrap.js +138 -0
  43. package/lib/commands/xliff.js +327 -0
  44. package/lib/commercial-eligibility.js +235 -0
  45. package/lib/concurrent.js +87 -0
  46. package/lib/config.js +523 -0
  47. package/lib/contamination-lane.js +76 -0
  48. package/lib/content-sync.js +731 -0
  49. package/lib/content.js +733 -0
  50. package/lib/corpus-registration.mjs +608 -0
  51. package/lib/cost-report.js +346 -0
  52. package/lib/diff.js +155 -0
  53. package/lib/docusaurus-sync.js +1256 -0
  54. package/lib/flatten.js +55 -0
  55. package/lib/format.js +954 -0
  56. package/lib/hash.js +159 -0
  57. package/lib/icu.js +473 -0
  58. package/lib/integrity.js +689 -0
  59. package/lib/license-gate.mjs +478 -0
  60. package/lib/license-identify.mjs +229 -0
  61. package/lib/lint.js +629 -0
  62. package/lib/method-manifest.js +60 -0
  63. package/lib/methods/anthropic.js +140 -0
  64. package/lib/methods/apertium.js +163 -0
  65. package/lib/methods/api.js +316 -0
  66. package/lib/methods/base.js +184 -0
  67. package/lib/methods/content-separator.js +45 -0
  68. package/lib/methods/deepl.js +426 -0
  69. package/lib/methods/direct-llm.js +586 -0
  70. package/lib/methods/external.js +332 -0
  71. package/lib/methods/fetch-with-retry.js +124 -0
  72. package/lib/methods/gemini.js +147 -0
  73. package/lib/methods/google-translate.js +402 -0
  74. package/lib/methods/http-utils.js +122 -0
  75. package/lib/methods/libretranslate.js +314 -0
  76. package/lib/methods/llm-coached.js +670 -0
  77. package/lib/methods/llm.js +592 -0
  78. package/lib/methods/local.js +76 -0
  79. package/lib/methods/microsoft-translator.js +331 -0
  80. package/lib/methods/openai.js +131 -0
  81. package/lib/methods/openrouter-client.js +327 -0
  82. package/lib/methods/openrouter-pricing.js +156 -0
  83. package/lib/methods/provider-env.js +115 -0
  84. package/lib/methods/provider-pricing.js +310 -0
  85. package/lib/methods/tilde.js +150 -0
  86. package/lib/methods/translated.js +229 -0
  87. package/lib/methods/translation-error.js +80 -0
  88. package/lib/models.js +258 -0
  89. package/lib/no-translate.js +233 -0
  90. package/lib/output.js +238 -0
  91. package/lib/pairs.js +547 -0
  92. package/lib/plugins.js +447 -0
  93. package/lib/provenance.js +323 -0
  94. package/lib/recommend.js +648 -0
  95. package/lib/registers.js +1185 -0
  96. package/lib/repair-script.js +266 -0
  97. package/lib/scripts.js +994 -0
  98. package/lib/seal.mjs +464 -0
  99. package/lib/sealed-qualifier.mjs +211 -0
  100. package/lib/security.js +59 -0
  101. package/lib/segment.js +369 -0
  102. package/lib/seo.js +275 -0
  103. package/lib/serve.js +854 -0
  104. package/lib/string-classify.js +85 -0
  105. package/lib/submit.mjs +344 -0
  106. package/lib/sync.js +969 -0
  107. package/lib/tags/bcp47.js +202 -0
  108. package/lib/tags/resolve.js +314 -0
  109. package/lib/terminology.js +111 -0
  110. package/lib/tm-seed.js +294 -0
  111. package/lib/tm.js +515 -0
  112. package/lib/translate-pair.js +197 -0
  113. package/lib/translate.js +203 -0
  114. package/lib/types.js +230 -0
  115. package/lib/validate.js +510 -0
  116. package/lib/verify.js +451 -0
  117. package/lib/watch.js +145 -0
  118. package/lib/xliff.js +184 -0
  119. package/package.json +93 -0
  120. package/shared/ATTRIBUTION.md +145 -0
  121. package/shared/CORPORA-CARDS.md +288 -0
  122. package/shared/DATA-SOVEREIGNTY.md +500 -0
  123. package/shared/LANGUAGE-CARD-FIELDS.md +532 -0
  124. package/shared/card-lint-baseline.json +3189 -0
  125. package/shared/cards-fallback.json +1 -0
  126. package/shared/catalogue/card-config.json +6091 -0
  127. package/shared/catalogue/external-results.json +3888 -0
  128. package/shared/catalogue/gender-guidance.json +1038 -0
  129. package/shared/catalogue/method-coverage.json +1751 -0
  130. package/shared/catalogue/metric-coverage.json +170 -0
  131. package/shared/catalogue/metric-reliability.json +1 -0
  132. package/shared/catalogue/register-presets.json +3180 -0
  133. package/shared/catalogue/vitality-scales.json +55 -0
  134. package/shared/cldr-index.json +1115 -0
  135. package/shared/code-bridge.json +253 -0
  136. package/shared/corpora-cards-v1-reference.md +281 -0
  137. package/shared/curated-dictionary-flags.json +35 -0
  138. package/shared/curated-endonyms.json +35 -0
  139. package/shared/curated-fsts.json +51 -0
  140. package/shared/curated-orthography-conventions.json +26 -0
  141. package/shared/curated-sil-resources.json +374 -0
  142. package/shared/curated-tools.json +41 -0
  143. package/shared/docent/corpus.json +11333 -0
  144. package/shared/docent/faq.en.json +564 -0
  145. package/shared/docent/register-blocks.json +60 -0
  146. package/shared/docent/system-prompt.md +144 -0
  147. package/shared/domain-taxonomy.json +35 -0
  148. package/shared/explainers/glossary.json +2975 -0
  149. package/shared/explainers/tc-features.json +20112 -0
  150. package/shared/explainers/term-watchlist.json +147 -0
  151. package/shared/human-services.json +59 -0
  152. package/shared/license-corrections.json +261 -0
  153. package/shared/license-evidence.json +13452 -0
  154. package/shared/licenses.json +6781 -0
  155. package/shared/method-registry.json +236 -0
  156. package/shared/metric-registry.json +620 -0
  157. package/shared/model-aliases.json +7 -0
  158. package/shared/schemas/champollion-plugin.schema.json +206 -0
  159. package/shared/schemas/corpora-card.schema.json +957 -0
  160. package/shared/schemas/domain-taxonomy.schema.json +64 -0
  161. package/shared/schemas/external-results.schema.json +314 -0
  162. package/shared/schemas/human-services.schema.json +90 -0
  163. package/shared/schemas/language-card.schema.json +1308 -0
  164. package/shared/schemas/licenses.schema.json +155 -0
  165. package/shared/schemas/method-card.schema.json +412 -0
  166. package/shared/schemas/method-registry.schema.json +85 -0
  167. package/shared/schemas/metric-registry.schema.json +96 -0
  168. package/shared/schemas/metric-reliability.schema.json +178 -0
  169. package/shared/schemas/model-aliases.schema.json +27 -0
  170. package/shared/schemas/source-snapshot.schema.json +96 -0
@@ -0,0 +1,331 @@
1
+ import { TranslationMethod } from './base.js';
2
+ import { getEnvOrFileVar } from '../api-key.js';
3
+ import { resolveProviderEnv, envNamesLabel } from './provider-env.js';
4
+ import { EST_CHARS_PER_KEY, DEFAULT_METHOD_CONCURRENCY } from '../config.js';
5
+ import { estimateProviderCost } from './provider-pricing.js';
6
+ import { extractContentBody } from './content-separator.js';
7
+ import { output } from '../output.js';
8
+ import { fetchWithRetry } from './fetch-with-retry.js';
9
+ import { pMap } from '../concurrent.js';
10
+
11
+ // Global default endpoint base (no query string). A custom-domain / VNet /
12
+ // private-endpoint Azure resource overrides this via MICROSOFT_TRANSLATOR_ENDPOINT
13
+ // (see _resolveEndpoint). Kept separate from the api-version so we build the
14
+ // query the same way the Python harness adapter does
15
+ // (arena/mt_eval_harness/methods/microsoft_translator.py — _resolve_endpoint /
16
+ // _build_request), so a sovereign/private deployment that works there also
17
+ // works here.
18
+ const MICROSOFT_API_BASE = 'https://api.cognitive.microsofttranslator.com/translate';
19
+ const MICROSOFT_API_VERSION = '3.0';
20
+ const MICROSOFT_REQUEST_TIMEOUT_MS = 15000;
21
+
22
+ class MicrosoftTranslatorMethod extends TranslationMethod {
23
+ constructor(options = {}) {
24
+ super('microsoft-translator', options);
25
+ }
26
+
27
+ // ── API resolution helpers ──────────────────────────────────────
28
+
29
+ /**
30
+ * Resolve the Microsoft Translator API key from options, env vars, or .env files.
31
+ * Checks the provider-env SSOT: MICROSOFT_TRANSLATOR_API_KEY (canonical),
32
+ * then AZURE_TRANSLATOR_KEY (alias). Accepting the alias here is what keeps
33
+ * the loader in step with readiness/doctor — a user who set only
34
+ * AZURE_TRANSLATOR_KEY now actually runs instead of silently producing null.
35
+ * @param {object} options - Caller-provided options
36
+ * @returns {string|null}
37
+ */
38
+ _resolveApiKey(options) {
39
+ return options.microsoftApiKey
40
+ || resolveProviderEnv('microsoft-translator', options.cwd).value;
41
+ }
42
+
43
+ /**
44
+ * Resolve the Azure region for the Translator resource.
45
+ * @param {object} options - Caller-provided options
46
+ * @returns {string|null}
47
+ */
48
+ _resolveRegion(options) {
49
+ return options.microsoftRegion
50
+ || getEnvOrFileVar('MICROSOFT_TRANSLATOR_REGION')
51
+ || getEnvOrFileVar('MICROSOFT_TRANSLATOR_REGION', options.cwd);
52
+ }
53
+
54
+ /**
55
+ * Resolve the Microsoft Translator /translate endpoint.
56
+ *
57
+ * Defaults to the global endpoint. A custom-domain / VNet / private-endpoint
58
+ * (sovereign) resource sets MICROSOFT_TRANSLATOR_ENDPOINT to its own URL —
59
+ * accepted either as a host base ("https://my-res.cognitiveservices.azure.com")
60
+ * OR a full ".../translate" path; both normalize to the same value. This is
61
+ * the SSOT env var the manifest declares and the Python harness honors
62
+ * (_resolve_endpoint); without this the CLI silently hit the global server for
63
+ * a private deployment.
64
+ * @param {object} options - Caller-provided options
65
+ * @returns {string} normalized endpoint base (no query string)
66
+ */
67
+ _resolveEndpoint(options = {}) {
68
+ let ep = options.microsoftEndpoint
69
+ || getEnvOrFileVar('MICROSOFT_TRANSLATOR_ENDPOINT')
70
+ || getEnvOrFileVar('MICROSOFT_TRANSLATOR_ENDPOINT', options.cwd);
71
+ if (!ep) return MICROSOFT_API_BASE;
72
+ ep = ep.replace(/\/+$/, '');
73
+ if (!ep.endsWith('/translate')) ep = `${ep}/translate`;
74
+ return ep;
75
+ }
76
+
77
+ /**
78
+ * Build the full request URL: <endpoint>?api-version=3.0&from=<src>&to=<tgt>.
79
+ * @param {string} endpoint - Resolved endpoint base (from _resolveEndpoint)
80
+ * @param {string} sourceLocale
81
+ * @param {string} targetLocale
82
+ * @returns {string}
83
+ */
84
+ _buildUrl(endpoint, sourceLocale, targetLocale) {
85
+ const params = new URLSearchParams({
86
+ 'api-version': MICROSOFT_API_VERSION,
87
+ from: sourceLocale,
88
+ to: targetLocale,
89
+ });
90
+ return `${endpoint}?${params.toString()}`;
91
+ }
92
+
93
+ /**
94
+ * Translate a batch of key-value pairs via Microsoft Translator API.
95
+ *
96
+ * @param {string[]} keys - Flat dot-notation keys to translate
97
+ * @param {object} sourceFlat - Full flattened source locale
98
+ * @param {import('../types.js').PairConfig} pairConfig - Pair config
99
+ * @param {object} options - { apiKey, batchSize }
100
+ * @returns {object|null} Map of key → translated value, or null
101
+ */
102
+ async translate(keys, sourceFlat, pairConfig, options) {
103
+ const apiKey = this._resolveApiKey(options);
104
+ const region = this._resolveRegion(options);
105
+ const endpoint = this._resolveEndpoint(options);
106
+
107
+ if (!apiKey) {
108
+ output.warn('Microsoft Translator: no API key — skipping.');
109
+ return null;
110
+ }
111
+
112
+ const targetLocale = pairConfig.target;
113
+ const sourceLocale = pairConfig.source || 'en';
114
+ const allTranslated = {};
115
+
116
+ // Microsoft supports up to 100 segments per request
117
+ const maxSegments = pairConfig.batchSize || options.batchSize || 100;
118
+
119
+ const batchChunks = [];
120
+ for (let i = 0; i < keys.length; i += maxSegments) {
121
+ batchChunks.push(keys.slice(i, i + maxSegments));
122
+ }
123
+
124
+ await pMap(batchChunks, async (chunk, idx) => {
125
+ const orderedKeys = [];
126
+ const sourceTexts = [];
127
+
128
+ for (const key of chunk) {
129
+ const val = sourceFlat[key];
130
+ if (val && typeof val === 'string') {
131
+ orderedKeys.push(key);
132
+ sourceTexts.push(val);
133
+ }
134
+ }
135
+
136
+ if (sourceTexts.length === 0) return;
137
+
138
+ const result = await this._translateBatchWithRetry({
139
+ apiKey,
140
+ region,
141
+ endpoint,
142
+ orderedKeys,
143
+ sourceTexts,
144
+ sourceLocale,
145
+ targetLocale,
146
+ batchNum: idx + 1,
147
+ });
148
+
149
+ if (result) {
150
+ Object.assign(allTranslated, result);
151
+ }
152
+ }, { concurrency: DEFAULT_METHOD_CONCURRENCY });
153
+
154
+ return Object.keys(allTranslated).length > 0 ? allTranslated : null;
155
+ }
156
+
157
+ /**
158
+ * Translate freeform Markdown content via Microsoft Translator API.
159
+ *
160
+ * Same protect/restore approach — ⟦PROTECTED_N⟧ placeholders shield
161
+ * code blocks and shortcodes from the translation engine.
162
+ *
163
+ * @param {string} prompt - Complete translation prompt from buildContentPrompt()
164
+ * @param {import('../types.js').PairConfig} pairConfig - Pair config
165
+ * @param {object} options - { apiKey }
166
+ * @returns {string|null} Translated text, or null on failure
167
+ */
168
+ async translateContent(prompt, pairConfig, options) {
169
+ const apiKey = this._resolveApiKey(options);
170
+ const region = this._resolveRegion(options);
171
+
172
+ if (!apiKey) {
173
+ output.warn('Microsoft Translator: no API key — skipping.');
174
+ return null;
175
+ }
176
+
177
+ // Extract the Markdown body from the translation prompt
178
+ const bodyText = extractContentBody(prompt);
179
+ if (!bodyText.trim()) return null;
180
+
181
+ const targetLocale = pairConfig.target;
182
+ const sourceLocale = pairConfig.source || 'en';
183
+
184
+ const endpoint = this._resolveEndpoint(options);
185
+ const url = this._buildUrl(endpoint, sourceLocale, targetLocale);
186
+ const headers = {
187
+ 'Content-Type': 'application/json; charset=UTF-8',
188
+ 'Ocp-Apim-Subscription-Key': apiKey,
189
+ };
190
+ if (region) {
191
+ headers['Ocp-Apim-Subscription-Region'] = region;
192
+ }
193
+
194
+ const response = await fetchWithRetry(url, {
195
+ method: 'POST',
196
+ headers,
197
+ body: JSON.stringify([{ Text: bodyText }]),
198
+ }, {
199
+ label: 'Microsoft content',
200
+ timeoutMs: MICROSOFT_REQUEST_TIMEOUT_MS * 2,
201
+ });
202
+
203
+ if (!response) return null;
204
+
205
+ if (!response.ok) {
206
+ const errorBody = await response.text();
207
+ output.error(`Microsoft content: ${response.status} — ${errorBody}`);
208
+ return null;
209
+ }
210
+
211
+ const json = await response.json();
212
+ if (!json || json.length === 0 || !json[0].translations?.[0]?.text) {
213
+ output.error('Microsoft content: empty response');
214
+ return null;
215
+ }
216
+
217
+ return json[0].translations[0].text;
218
+ }
219
+
220
+ estimateCost(keyCount) {
221
+ return estimateProviderCost('microsoft-translator', keyCount);
222
+ }
223
+
224
+ checkReadiness(context = {}) {
225
+ // Resolve through the same SSOT the loader uses so readiness can never
226
+ // pass under a name translate() won't read (or vice versa).
227
+ const { value } = resolveProviderEnv('microsoft-translator', context.cwd);
228
+ if (!value) {
229
+ return { ready: false, reason: `No Microsoft Translator key (${envNamesLabel('microsoft-translator')}).` };
230
+ }
231
+ return { ready: true };
232
+ }
233
+
234
+ getQualityTier() {
235
+ return 'standard';
236
+ }
237
+
238
+ getProvenance() {
239
+ return {
240
+ resources: [
241
+ {
242
+ name: 'Microsoft Translator API',
243
+ license: 'Proprietary (Microsoft ToS)',
244
+ type: 'api',
245
+ },
246
+ ],
247
+ commercialReady: true,
248
+ flags: [],
249
+ };
250
+ }
251
+
252
+ getSetupHelp() {
253
+ const { value: apiKey } = resolveProviderEnv('microsoft-translator');
254
+ if (!apiKey) {
255
+ return [
256
+ '',
257
+ ' ┌─ Missing API Key ─────────────────────────────────────────────┐',
258
+ ' │ Microsoft Translator requires an Azure subscription key. │',
259
+ ' │ │',
260
+ ' │ 1. Create a Translator resource in Azure Portal │',
261
+ ' │ 2. Run: export MICROSOFT_TRANSLATOR_API_KEY=... │',
262
+ ' │ 3. Set region: export MICROSOFT_TRANSLATOR_REGION=eastus │',
263
+ ' │ │',
264
+ ' │ Docs: https://learn.microsoft.com/azure/cognitive-services/ │',
265
+ ' │ translator/quickstart-text-rest-api │',
266
+ ' └────────────────────────────────────────────────────────────────┘',
267
+ ];
268
+ }
269
+ return this._apiFailureHelp('Azure Portal');
270
+ }
271
+
272
+ async _translateBatchWithRetry({
273
+ apiKey,
274
+ region,
275
+ endpoint,
276
+ orderedKeys,
277
+ sourceTexts,
278
+ sourceLocale,
279
+ targetLocale,
280
+ batchNum,
281
+ }) {
282
+ const url = this._buildUrl(endpoint, sourceLocale, targetLocale);
283
+
284
+ const headers = {
285
+ 'Content-Type': 'application/json; charset=UTF-8',
286
+ 'Ocp-Apim-Subscription-Key': apiKey,
287
+ };
288
+
289
+ if (region) {
290
+ headers['Ocp-Apim-Subscription-Region'] = region;
291
+ }
292
+
293
+ const body = sourceTexts.map(text => ({ Text: text }));
294
+
295
+ const response = await fetchWithRetry(url, {
296
+ method: 'POST',
297
+ headers,
298
+ body: JSON.stringify(body),
299
+ }, {
300
+ label: `Microsoft Translator batch ${batchNum}`,
301
+ timeoutMs: MICROSOFT_REQUEST_TIMEOUT_MS,
302
+ });
303
+
304
+ if (!response) return null;
305
+
306
+ if (!response.ok) {
307
+ const errorBody = await response.text();
308
+ output.error(`Microsoft Translator batch ${batchNum}: ${response.status} — ${errorBody}`);
309
+ return null;
310
+ }
311
+
312
+ const json = await response.json();
313
+
314
+ if (!json || json.length !== orderedKeys.length) {
315
+ output.error(`Microsoft Translator batch ${batchNum}: Response length mismatch (expected ${orderedKeys.length}, got ${json?.length || 0})`);
316
+ return null;
317
+ }
318
+
319
+ const result = {};
320
+ for (let i = 0; i < orderedKeys.length; i++) {
321
+ result[orderedKeys[i]] = json[i].translations[0].text;
322
+ }
323
+
324
+ const charCount = sourceTexts.reduce((sum, t) => sum + t.length, 0);
325
+ output.progress(` ✓ Microsoft batch ${batchNum} (${orderedKeys.length} keys, ${charCount} chars)`);
326
+
327
+ return result;
328
+ }
329
+ }
330
+
331
+ export { MicrosoftTranslatorMethod };
@@ -0,0 +1,131 @@
1
+ /**
2
+ * OpenAI Translation Method — direct OpenAI Chat Completions API.
3
+ *
4
+ * Extends DirectLLMMethod to provide OpenAI-specific:
5
+ * - API endpoint (https://api.openai.com/v1/chat/completions)
6
+ * - Auth header format (Bearer token)
7
+ * - Request body (messages array, response_format for JSON mode)
8
+ * - Response parsing (choices[0].message.content)
9
+ * - Model listing via GET /v1/models
10
+ * - Pricing (gpt-4o, gpt-4o-mini)
11
+ *
12
+ * All shared logic (translate, translateContent, coaching, retry, validation)
13
+ * lives in DirectLLMMethod.
14
+ */
15
+
16
+ import { DirectLLMMethod } from './direct-llm.js';
17
+ import { fetchAvailableModels } from '../models.js';
18
+ import { estimateLlmCost } from './provider-pricing.js';
19
+
20
+ // The default model, named ONCE — it was previously written twice (here and
21
+ // as an inline fallback in estimateCost()) and the two could drift.
22
+ const DEFAULT_MODEL = 'gpt-4o';
23
+
24
+ class OpenAIMethod extends DirectLLMMethod {
25
+ constructor(options = {}) {
26
+ super(options);
27
+ this.name = 'openai';
28
+ }
29
+
30
+ // ── Provider identity ────────────────────────────────────────────
31
+
32
+ _getApiKeyEnvVar() { return 'OPENAI_API_KEY'; }
33
+ _getApiKeyOptionsKey() { return 'openaiApiKey'; }
34
+ _getDefaultModel() { return DEFAULT_MODEL; }
35
+ _getProviderLabel() { return 'OpenAI'; }
36
+ _getDefaultApiBase() { return 'https://api.openai.com/v1'; }
37
+
38
+ // ── API request/response shape ───────────────────────────────────
39
+
40
+ _buildApiRequest({ prompt, systemMessage, apiKey, model, temperature, isJsonMode }) {
41
+ const messages = systemMessage
42
+ ? [{ role: 'system', content: systemMessage }, { role: 'user', content: prompt }]
43
+ : [{ role: 'user', content: prompt }];
44
+
45
+ const body = { model, messages, temperature };
46
+ if (isJsonMode) {
47
+ body.response_format = { type: 'json_object' };
48
+ }
49
+
50
+ const base = this._resolveApiBase() || 'https://api.openai.com/v1';
51
+ return {
52
+ url: `${base}/chat/completions`,
53
+ headers: {
54
+ 'Authorization': `Bearer ${apiKey}`,
55
+ 'Content-Type': 'application/json',
56
+ },
57
+ body,
58
+ };
59
+ }
60
+
61
+ _extractResponseText(json) {
62
+ return json.choices?.[0]?.message?.content || null;
63
+ }
64
+
65
+ // ── Runtime model listing ────────────────────────────────────────
66
+
67
+ async _fetchModels(apiKey) {
68
+ // Delegate to the shared models.js module — single source of truth for
69
+ // model listing used by init wizard, `champollion models`, and validation.
70
+ return fetchAvailableModels('openai', apiKey);
71
+ }
72
+
73
+ // ── Model-aware quality tier ─────────────────────────────────────
74
+
75
+ _getModelTier(model) {
76
+ if (model.includes('mini')) return 'budget';
77
+ if (model.startsWith('o1') || model.startsWith('o3') || model.startsWith('o4')) return 'premium';
78
+ return 'standard'; // gpt-4o and similar
79
+ }
80
+
81
+ // ── Pricing ──────────────────────────────────────────────────────
82
+
83
+ async estimateCost(keyCount, pairConfig = {}) {
84
+ return estimateLlmCost('openai', pairConfig.model || DEFAULT_MODEL, keyCount);
85
+ }
86
+
87
+ // ── Provenance ───────────────────────────────────────────────────
88
+
89
+ checkReadiness(context) {
90
+ // Resolve through the same chain translate() uses (options → env →
91
+ // .env.local/.env in cwd) so readiness can never fail for a key the
92
+ // loader *would* read — e.g. OPENAI_API_KEY set only in .env.local.
93
+ if (!this._resolveApiKey(context || {})) {
94
+ return { ready: false, reason: 'No OpenAI API key (OPENAI_API_KEY).' };
95
+ }
96
+ return { ready: true };
97
+ }
98
+
99
+ getProvenance() {
100
+ return {
101
+ resources: [
102
+ {
103
+ name: 'OpenAI Developer API',
104
+ license: 'Proprietary (OpenAI ToS)',
105
+ type: 'api',
106
+ },
107
+ ],
108
+ commercialReady: true,
109
+ flags: [],
110
+ };
111
+ }
112
+
113
+ getSetupHelp() {
114
+ const apiKey = process.env.OPENAI_API_KEY;
115
+ if (!apiKey) {
116
+ return [
117
+ '',
118
+ ' ┌─ Missing API Key ─────────────────────────────────────────────┐',
119
+ ' │ The OpenAI method requires an OpenAI API key. │',
120
+ ' │ │',
121
+ ' │ 1. Sign up at https://platform.openai.com │',
122
+ ' │ 2. Run: export OPENAI_API_KEY=sk-... │',
123
+ ' │ 3. Or add to .env.local: OPENAI_API_KEY=sk-... │',
124
+ ' └────────────────────────────────────────────────────────────────┘',
125
+ ];
126
+ }
127
+ return this._apiFailureHelp('OpenAI dashboard');
128
+ }
129
+ }
130
+
131
+ export { OpenAIMethod };