@@ -124,65 +124,85 @@ export function createTranslationRunner({
124124 // This request index points to complete artifacts, so reopening does not
125125 // need Python discovery, option introspection, or the service catalog.
126126 // Only a digest is persisted; service values may contain credentials.
127- const requestKey = sourceHash
128- . copy ( )
129- . update (
130- JSON . stringify ( {
131- cacheIndex : 1 ,
132- layoutSchema : currentLayoutSchema ,
133- id,
134- page,
135- language,
136- sourceLanguage : sourceLanguage || 'English' ,
137- glossary,
138- advancedOptions,
139- translationService : cacheSelection ,
140- serviceIdentity,
141- localTranslation,
142- cacheScope,
143- } ) ,
144- )
145- . digest ( 'hex' ) ;
146- const requestPath = join ( cacheDir , requestKey + '.page.json' ) ;
147- const rememberPage = async ( artifactKey , translationModel ) => {
148- const temporary = requestPath + '.' + crypto . randomUUID ( ) + '.tmp' ;
127+ const requestDigest = ( serviceBound ) =>
128+ sourceHash
129+ . copy ( )
130+ . update (
131+ JSON . stringify ( {
132+ cacheIndex : 1 ,
133+ layoutSchema : currentLayoutSchema ,
134+ id,
135+ page,
136+ language,
137+ sourceLanguage : sourceLanguage || 'English' ,
138+ glossary,
139+ advancedOptions,
140+ ...( serviceBound
141+ ? { translationService : cacheSelection , serviceIdentity, localTranslation }
142+ : { compatible : 1 } ) ,
143+ cacheScope,
144+ } ) ,
145+ )
146+ . digest ( 'hex' ) ;
147+ const requestPath = join ( cacheDir , requestDigest ( true ) + '.page.json' ) ;
148+ // The compatible index omits the service, so any service can reuse it.
149+ const compatibleRequestPath = join ( cacheDir , requestDigest ( false ) + '.page.json' ) ;
150+ const writeIndex = async ( path , value ) => {
151+ const temporary = path + '.' + crypto . randomUUID ( ) + '.tmp' ;
149152 try {
150153 await mkdir ( cacheDir , { recursive : true } ) ;
151- await writeFile ( temporary , JSON . stringify ( { key : artifactKey , model : translationModel } ) ) ;
152- await rename ( temporary , requestPath ) ;
154+ await writeFile ( temporary , JSON . stringify ( value ) ) ;
155+ await rename ( temporary , path ) ;
153156 } finally {
154157 await rm ( temporary , { force : true } ) . catch ( ( ) => { } ) ;
155158 }
156159 } ;
160+ const rememberPage = async ( artifactKey , translationModel , reused = false ) => {
161+ await writeIndex ( requestPath , { key : artifactKey , model : translationModel , reused } ) ;
162+ await writeIndex ( compatibleRequestPath , { key : artifactKey , model : translationModel } ) ;
163+ } ;
157164 if ( ! forceRetranslation ) {
158- try {
159- const saved = JSON . parse ( await readFile ( requestPath , 'utf8' ) ) ;
160- if ( ! / ^ [ 0 - 9 a - f ] { 64 } $ / . test ( saved . key ) || typeof saved . model !== 'string' )
161- throw Error ( 'Invalid page cache index' ) ;
162- // Without reuse, only the requested model's own artifact qualifies.
163- if ( ! reuseTranslations && saved . model !== model ) throw Error ( 'Different cached model' ) ;
164- const [ result , metadata ] = await Promise . all ( [
165- readFile ( join ( cacheDir , saved . key + '.pdf' ) ) ,
166- readFile ( join ( cacheDir , saved . key + '.layout.json' ) , 'utf8' ) ,
167- ] ) ;
168- if (
169- ! result . subarray ( 0 , 1024 ) . includes ( Buffer . from ( '%PDF' ) ) ||
170- ! Array . isArray ( JSON . parse ( metadata ) . paragraphs )
171- )
172- throw Error ( 'Invalid cached page' ) ;
173- result . layoutKey = cacheScope ? `${ scopeHash } -${ scopeGeneration } -${ saved . key } ` : saved . key ;
174- result . cached = true ;
175- result . translationModel = saved . model ;
176- step ( 'cacheLookup' ) ;
177- emitTiming ( {
178- engine : id ,
179- cached : true ,
180- model : saved . model ,
181- totalMs : performance . now ( ) - started ,
182- stages,
183- } ) ;
184- return result ;
185- } catch { }
165+ for ( const indexPath of reuseTranslations
166+ ? [ requestPath , compatibleRequestPath ]
167+ : [ requestPath ] ) {
168+ try {
169+ const saved = JSON . parse ( await readFile ( indexPath , 'utf8' ) ) ;
170+ if ( ! / ^ [ 0 - 9 a - f ] { 64 } $ / . test ( saved . key ) || typeof saved . model !== 'string' )
171+ throw Error ( 'Invalid page cache index' ) ;
172+ // Without reuse, only the requested model's own artifact qualifies.
173+ if ( ! reuseTranslations && ( saved . model !== model || saved . reused ) )
174+ throw Error ( 'Different cached model' ) ;
175+ const [ result , metadata ] = await Promise . all ( [
176+ readFile ( join ( cacheDir , saved . key + '.pdf' ) ) ,
177+ readFile ( join ( cacheDir , saved . key + '.layout.json' ) , 'utf8' ) ,
178+ ] ) ;
179+ if (
180+ ! result . subarray ( 0 , 1024 ) . includes ( Buffer . from ( '%PDF' ) ) ||
181+ ! Array . isArray ( JSON . parse ( metadata ) . paragraphs )
182+ )
183+ throw Error ( 'Invalid cached page' ) ;
184+ if ( indexPath === compatibleRequestPath )
185+ await writeIndex ( requestPath , {
186+ key : saved . key ,
187+ model : saved . model ,
188+ reused : true ,
189+ } ) . catch ( ( ) => { } ) ;
190+ result . layoutKey = cacheScope
191+ ? `${ scopeHash } -${ scopeGeneration } -${ saved . key } `
192+ : saved . key ;
193+ result . cached = true ;
194+ result . translationModel = saved . model ;
195+ step ( 'cacheLookup' ) ;
196+ emitTiming ( {
197+ engine : id ,
198+ cached : true ,
199+ model : saved . model ,
200+ totalMs : performance . now ( ) - started ,
201+ stages,
202+ } ) ;
203+ return result ;
204+ } catch { }
205+ }
186206 }
187207 // A cache miss is not kernel work. The actual translation request can still
188208 // migrate legacy artifacts below, but this fast lookup must never probe Python.
@@ -200,7 +220,7 @@ export function createTranslationRunner({
200220 if ( service ) model = service . model ;
201221 step ( 'environmentAndOptions' ) ;
202222 const layoutKey = ( key ) => ( cacheScope ? `${ scopeHash } -${ scopeGeneration } -${ key } ` : key ) ;
203- const keyFor = ( cacheModel , layoutSchema = currentLayoutSchema ) =>
223+ const keyFor = ( cacheModel , layoutSchema = currentLayoutSchema , serviceBound = true ) =>
204224 sourceHash
205225 . copy ( )
206226 . update (
@@ -211,9 +231,11 @@ export function createTranslationRunner({
211231 language,
212232 ...( sourceLanguage && sourceLanguage !== 'English' ? { sourceLanguage } : { } ) ,
213233 model : cacheModel ,
214- ...( service || serviceIdentity
215- ? { service : service ?. cacheIdentity || serviceIdentity }
216- : { } ) ,
234+ ...( ! serviceBound
235+ ? { compatible : 1 }
236+ : service || serviceIdentity
237+ ? { service : service ?. cacheIdentity || serviceIdentity }
238+ : { } ) ,
217239 prompt : 2 ,
218240 ...( glossary . length ? { glossary } : { } ) ,
219241 ...( cacheScope ? { cacheScope } : { } ) ,
@@ -226,6 +248,7 @@ export function createTranslationRunner({
226248 const cache = createTranslationCache ( {
227249 directory : cacheDir ,
228250 keyFor,
251+ compatibleKey : keyFor ( undefined , currentLayoutSchema , false ) ,
229252 fallbackKeyFors :
230253 currentLayoutSchema >= 5
231254 ? [ ]
@@ -242,7 +265,7 @@ export function createTranslationRunner({
242265 const cached = join ( cacheDir , `${ key } .pdf` ) ;
243266 const hit = await cache . lookup ( model , { reuseTranslations, forceRetranslation } ) ;
244267 if ( hit ) {
245- await rememberPage ( hit . key , hit . model ) . catch ( ( ) => { } ) ;
268+ await rememberPage ( hit . key , hit . model , hit . reused ) . catch ( ( ) => { } ) ;
246269 const result = hit . result ;
247270 result . layoutKey = layoutKey ( hit . key ) ;
248271 result . cached = true ;
@@ -267,7 +290,7 @@ export function createTranslationRunner({
267290 const queuedHit = await cache . lookup ( model , { reuseTranslations, forceRetranslation } ) ;
268291 step ( 'cacheRecheck' ) ;
269292 if ( queuedHit ) {
270- await rememberPage ( queuedHit . key , queuedHit . model ) . catch ( ( ) => { } ) ;
293+ await rememberPage ( queuedHit . key , queuedHit . model , queuedHit . reused ) . catch ( ( ) => { } ) ;
271294 const result = queuedHit . result ;
272295 result . layoutKey = layoutKey ( queuedHit . key ) ;
273296 result . cached = true ;
0 commit comments