@startsimpli/llm 0.4.16 → 0.4.18
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/package.json +5 -3
- package/src/__tests__/openai-provider.test.ts +126 -0
- package/src/providers/openai.ts +31 -6
- package/src/translation/__fixtures__/golden-pairs.ts +243 -0
- package/src/translation/__tests__/batch-contract.test.ts +247 -0
- package/src/translation/__tests__/quality-gates.test.ts +338 -0
- package/src/translation/__tests__/route-confidentiality.test.ts +228 -0
- package/src/translation/__tests__/stub-provider.ts +118 -0
- package/src/translation/index.ts +784 -0
- package/src/translation/types.ts +300 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The translation engine's CONTRACT (bd startsim-ob4d, RED).
|
|
3
|
+
*
|
|
4
|
+
* Types only. The behaviour is stubbed in `./index` and unimplemented on
|
|
5
|
+
* purpose — the tests in `./__tests__` are written against this surface first
|
|
6
|
+
* and are expected to fail until bd startsim-h1q8 (GREEN) implements it.
|
|
7
|
+
*
|
|
8
|
+
* THE THREE DESIGN DECISIONS THIS FILE ENCODES, so a reader does not have to
|
|
9
|
+
* rediscover them from the tests:
|
|
10
|
+
*
|
|
11
|
+
* `docs/design/translation/startsim-yfot-…` — the batch is N segments in ONE
|
|
12
|
+
* prompt with echoed ids, cut by CHARACTER budget, and the id-echo contract
|
|
13
|
+
* is exact: the returned id set equals the requested id set.
|
|
14
|
+
*
|
|
15
|
+
* `docs/design/translation/startsim-r9q8-…` — quality is proven by comparing
|
|
16
|
+
* SOURCE TO OUTPUT, never by judging the output's language. Hence
|
|
17
|
+
* `MechanicalChecks`: glossary adherence, do-not-translate, numeric fidelity.
|
|
18
|
+
*
|
|
19
|
+
* `docs/design/translation/startsim-bfem-…` — assert invariants, never
|
|
20
|
+
* wording. `temperature: 0` is not available on the default model, so nothing
|
|
21
|
+
* here may assume a reproducible string.
|
|
22
|
+
*
|
|
23
|
+
* NO LOCALE IS NAMED ANYWHERE IN THIS PACKAGE. `sourceLocale`/`targetLocale`
|
|
24
|
+
* are opaque BCP-47 strings supplied per request. A branch on a locale code is
|
|
25
|
+
* the leak `startsim-jb1z` bans, and the two-locale fixture set exists to catch
|
|
26
|
+
* one.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { ILLMProvider } from '../providers/interface';
|
|
30
|
+
|
|
31
|
+
/** A BCP-47 tag. Data, never a branch. */
|
|
32
|
+
export type Locale = string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One unit of text to translate.
|
|
36
|
+
*
|
|
37
|
+
* `id` IS OPAQUE. The engine assigns nothing, parses nothing out of it, and
|
|
38
|
+
* round-trips it byte-for-byte. The caller owns segmentation, so the caller
|
|
39
|
+
* owns the naming.
|
|
40
|
+
*/
|
|
41
|
+
export interface TranslationSegment {
|
|
42
|
+
id: string;
|
|
43
|
+
text: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A term the client has ruled on.
|
|
48
|
+
*
|
|
49
|
+
* `binding` means the approved `target` MUST appear when `source` occurred.
|
|
50
|
+
* `doNotTranslate` means `source` must survive VERBATIM — brand names, product
|
|
51
|
+
* names, legal entity names — and `target` is ignored for those.
|
|
52
|
+
*/
|
|
53
|
+
export interface GlossaryTerm {
|
|
54
|
+
source: string;
|
|
55
|
+
target: string;
|
|
56
|
+
binding: boolean;
|
|
57
|
+
doNotTranslate?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A previously approved pair, reusable by exact match. */
|
|
61
|
+
export interface MemoryUnit {
|
|
62
|
+
sourceText: string;
|
|
63
|
+
targetText: string;
|
|
64
|
+
sourceLocale: Locale;
|
|
65
|
+
targetLocale: Locale;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface TranslationRequest {
|
|
69
|
+
segments: TranslationSegment[];
|
|
70
|
+
sourceLocale: Locale;
|
|
71
|
+
targetLocale: Locale;
|
|
72
|
+
/** Only the terms occurring in THIS batch are injected. See `renderTranslatePrompt`. */
|
|
73
|
+
glossary?: readonly GlossaryTerm[];
|
|
74
|
+
/**
|
|
75
|
+
* Approved pairs, used two ways: an EXACT source match is served without a
|
|
76
|
+
* provider call at all, and the remainder ride along as labelled examples.
|
|
77
|
+
*
|
|
78
|
+
* THE CALLER BOUNDS THIS. The engine injects what it is given, so handing it a
|
|
79
|
+
* whole translation memory puts a whole translation memory in every prompt —
|
|
80
|
+
* the failure the glossary is filtered to avoid. `startsim-jb1z` specs the
|
|
81
|
+
* store side as `nearest(segmentText, targetLocale, limit)` for exactly this
|
|
82
|
+
* reason; the engine does not re-rank, because it has no retriever.
|
|
83
|
+
*/
|
|
84
|
+
memory?: readonly MemoryUnit[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* What produced a unit. Without it, a quality regression after a route change
|
|
89
|
+
* is unattributable (`startsim-jb1z`'s artifact model).
|
|
90
|
+
*/
|
|
91
|
+
export interface TranslationProvenance {
|
|
92
|
+
provider: string;
|
|
93
|
+
model: string;
|
|
94
|
+
promptVersion: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** One term's verdict. `present` is a substring question, not a judgement. */
|
|
98
|
+
export interface TermCheck {
|
|
99
|
+
term: string;
|
|
100
|
+
present: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The gates that run on EVERY batch in production, not only in tests
|
|
105
|
+
* (`startsim-r9q8`). All are computed source-to-output and need no reader of
|
|
106
|
+
* the target language.
|
|
107
|
+
*/
|
|
108
|
+
export interface MechanicalChecks {
|
|
109
|
+
glossaryAdherence: TermCheck[];
|
|
110
|
+
doNotTranslate: TermCheck[];
|
|
111
|
+
/** Numbers, dates and percentages in the source that are absent from the target. */
|
|
112
|
+
missingNumerics: string[];
|
|
113
|
+
/** False when any binding term, do-not-translate string or numeric is missing. */
|
|
114
|
+
ok: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface TranslatedSegment {
|
|
118
|
+
id: string;
|
|
119
|
+
/**
|
|
120
|
+
* Storage form. Carries NO bidi isolate characters — isolation happens at
|
|
121
|
+
* render time (`packages/i18n/src/bidi.ts`), so a stored string stays
|
|
122
|
+
* comparable.
|
|
123
|
+
*/
|
|
124
|
+
targetText: string;
|
|
125
|
+
provenance: TranslationProvenance;
|
|
126
|
+
/** True when served from memory, which means no provider call was made. */
|
|
127
|
+
fromMemory: boolean;
|
|
128
|
+
checks: MechanicalChecks;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type FailureReason =
|
|
132
|
+
| 'missing_id'
|
|
133
|
+
| 'empty_text'
|
|
134
|
+
| 'mechanical_check'
|
|
135
|
+
| 'provider_error';
|
|
136
|
+
|
|
137
|
+
export interface TranslationFailure {
|
|
138
|
+
id: string;
|
|
139
|
+
reason: FailureReason;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface TranslationResult {
|
|
143
|
+
translated: TranslatedSegment[];
|
|
144
|
+
failed: TranslationFailure[];
|
|
145
|
+
/** Ids re-queued individually after the batch response omitted them. */
|
|
146
|
+
requeued: string[];
|
|
147
|
+
/** Provider calls actually made. A memory-only request must report 0. */
|
|
148
|
+
providerCalls: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// the envelope
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The parse of one provider response, per `startsim-yfot`'s id-echo contract.
|
|
157
|
+
*
|
|
158
|
+
* A MISSING id is survivable — it fails only its own segment, which is
|
|
159
|
+
* re-queued. An UNKNOWN or DUPLICATE id is not: the model has attributed text
|
|
160
|
+
* to a segment that did not ask for it, and in a product whose selling point is
|
|
161
|
+
* that one client's material never appears against another's, that is a
|
|
162
|
+
* correctness alarm. Never map an unknown id positionally.
|
|
163
|
+
*/
|
|
164
|
+
export type EnvelopeRejection = 'unknown_id' | 'duplicate_id' | 'malformed';
|
|
165
|
+
|
|
166
|
+
export interface EnvelopeAccepted {
|
|
167
|
+
ok: true;
|
|
168
|
+
segments: TranslationSegment[];
|
|
169
|
+
/** Requested ids the response did not carry. Re-queued individually. */
|
|
170
|
+
missing: string[];
|
|
171
|
+
/** Present ids whose text was blank — a failure, never an empty translation. */
|
|
172
|
+
empty: string[];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface EnvelopeRejected {
|
|
176
|
+
ok: false;
|
|
177
|
+
reason: EnvelopeRejection;
|
|
178
|
+
/** The offending ids, for the operator. NEVER the response text. */
|
|
179
|
+
offendingIds: string[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type EnvelopeResult = EnvelopeAccepted | EnvelopeRejected;
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// structure
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* A document as the CALLER models it. Structure never enters the model
|
|
190
|
+
* (`startsim-jb1z`), so the engine segments and reassembles and the round trip
|
|
191
|
+
* is deterministic by construction.
|
|
192
|
+
*
|
|
193
|
+
* `kind` is free-form — `heading`, `paragraph`, `list_item`, `table_cell`,
|
|
194
|
+
* `code`. The engine does not interpret it; it carries it through.
|
|
195
|
+
*/
|
|
196
|
+
export interface DocumentNode {
|
|
197
|
+
kind: string;
|
|
198
|
+
/** Leaf text. Absent on a container. */
|
|
199
|
+
text?: string;
|
|
200
|
+
children?: DocumentNode[];
|
|
201
|
+
/** Nodes flagged here are carried through untranslated (product codes, ids). */
|
|
202
|
+
translatable?: boolean;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface StructuredDocument {
|
|
206
|
+
nodes: DocumentNode[];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The extraction result: the translatable leaves, plus whatever the
|
|
211
|
+
* reassembler needs to put them back. The skeleton is opaque to the caller.
|
|
212
|
+
*/
|
|
213
|
+
export interface SegmentedDocument {
|
|
214
|
+
segments: TranslationSegment[];
|
|
215
|
+
skeleton: StructuredDocument;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// batching
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Provisional, and explicitly so (`startsim-yfot`, Unverified). 4,000 chars sits
|
|
224
|
+
* below the MEASURED 6,000-char extraction budget because translation output
|
|
225
|
+
* scales with input and extraction's does not; 25 segments caps the id-matching
|
|
226
|
+
* burden on a page of one-line table cells.
|
|
227
|
+
*
|
|
228
|
+
* These are to be REPLACED by a measurement of translation itself.
|
|
229
|
+
*/
|
|
230
|
+
export interface BatchLimits {
|
|
231
|
+
maxCharsPerBatch: number;
|
|
232
|
+
maxSegmentsPerBatch: number;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export const DEFAULT_BATCH_LIMITS: BatchLimits = {
|
|
236
|
+
maxCharsPerBatch: 4_000,
|
|
237
|
+
maxSegmentsPerBatch: 25,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// routing
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The retention posture, which is what picks the provider — not the model's
|
|
246
|
+
* quality (`startsim-hopl`, `startsim-yfot`).
|
|
247
|
+
*
|
|
248
|
+
* `walled` carries the paying client's confidential material and CANNOT fall
|
|
249
|
+
* back: if the configured provider is unavailable the call fails and is retried
|
|
250
|
+
* later against the same provider. `open` carries material that is not
|
|
251
|
+
* confidential in that sense and may fall back within the posture.
|
|
252
|
+
*/
|
|
253
|
+
export type RetentionPosture = 'walled' | 'open';
|
|
254
|
+
|
|
255
|
+
export interface TranslationRoute {
|
|
256
|
+
posture: RetentionPosture;
|
|
257
|
+
provider: string;
|
|
258
|
+
model: string;
|
|
259
|
+
/** Boot-time allow-list. A model outside it must stop the service starting. */
|
|
260
|
+
allowedModels: readonly string[];
|
|
261
|
+
fallback: boolean;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Where the engine writes operational detail.
|
|
266
|
+
*
|
|
267
|
+
* IT IS AN INJECTED SEAM SO A TEST CAN CAPTURE IT. The confidentiality
|
|
268
|
+
* guarantee breaks on the failure path — `startsim-bfem` names the live
|
|
269
|
+
* precedent, a Django handler that logs the model's raw response on a
|
|
270
|
+
* JSON-parse failure — so "no client text reaches a log" has to be an
|
|
271
|
+
* assertion, not a code-review habit.
|
|
272
|
+
*/
|
|
273
|
+
export interface TranslationLogger {
|
|
274
|
+
debug(message: string, meta?: Record<string, unknown>): void;
|
|
275
|
+
info(message: string, meta?: Record<string, unknown>): void;
|
|
276
|
+
warn(message: string, meta?: Record<string, unknown>): void;
|
|
277
|
+
error(message: string, meta?: Record<string, unknown>): void;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export interface TranslationDeps {
|
|
281
|
+
provider: ILLMProvider;
|
|
282
|
+
logger?: TranslationLogger;
|
|
283
|
+
limits?: BatchLimits;
|
|
284
|
+
/** Attempts at a malformed envelope before the batch fails. */
|
|
285
|
+
maxEnvelopeRetries?: number;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export interface TranslationService {
|
|
289
|
+
translate(request: TranslationRequest): Promise<TranslationResult>;
|
|
290
|
+
readonly route: TranslationRoute;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Context the batch prompt is rendered from. */
|
|
294
|
+
export interface PromptContext {
|
|
295
|
+
sourceLocale: Locale;
|
|
296
|
+
targetLocale: Locale;
|
|
297
|
+
glossary?: readonly GlossaryTerm[];
|
|
298
|
+
/** Labelled separately so the model can tell an EXAMPLE from a thing to translate. */
|
|
299
|
+
memoryExamples?: readonly MemoryUnit[];
|
|
300
|
+
}
|