@qvac/harness 0.2.10 → 0.3.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 (41) hide show
  1. package/dist/harness.bundle +13 -11
  2. package/dist-lib/index.d.ts +26 -1
  3. package/dist-lib/index.js +94 -3
  4. package/dist-lib/lib/capabilities/completion.d.ts +2 -0
  5. package/dist-lib/lib/capabilities/detect-language.d.ts +11 -0
  6. package/dist-lib/lib/capabilities/detect-language.js +13 -0
  7. package/dist-lib/lib/capabilities/enhance.d.ts +10 -0
  8. package/dist-lib/lib/capabilities/enhance.js +29 -0
  9. package/dist-lib/lib/capabilities/invoke.d.ts +24 -0
  10. package/dist-lib/lib/capabilities/invoke.js +57 -0
  11. package/dist-lib/lib/connect.d.ts +4 -1
  12. package/dist-lib/lib/connect.js +50 -21
  13. package/dist-lib/lib/contract-types.d.ts +21 -0
  14. package/dist-lib/lib/helpers/clean-answer.d.ts +1 -0
  15. package/dist-lib/lib/helpers/clean-answer.js +16 -0
  16. package/dist-lib/lib/helpers/load-stall.d.ts +6 -0
  17. package/dist-lib/lib/helpers/load-stall.js +24 -0
  18. package/dist-lib/lib/hooks/built-in/sandbox.js +3 -1
  19. package/dist-lib/lib/models/model-pool.d.ts +6 -0
  20. package/dist-lib/lib/models/model-pool.js +12 -0
  21. package/dist-lib/lib/models/nmt-catalog.d.ts +34 -0
  22. package/dist-lib/lib/models/nmt-catalog.js +166 -0
  23. package/dist-lib/lib/serve.js +33 -4
  24. package/dist-lib/lib/tools/generate-image.js +2 -25
  25. package/dist-lib/lib/tools/http-request.js +1 -1
  26. package/dist-lib/lib/tools/skills/manifest-parse.d.ts +1 -0
  27. package/dist-lib/lib/tools/skills/manifest-parse.js +92 -1
  28. package/dist-lib/lib/tools/skills/manifest.d.ts +12 -0
  29. package/dist-lib/lib/tools/translate.d.ts +78 -0
  30. package/dist-lib/lib/tools/translate.js +228 -0
  31. package/dist-lib/lib/tools/web-fetch.js +12 -2
  32. package/dist-lib/schema/harness.js +53 -3
  33. package/dist-lib/spec/bind.js +3 -0
  34. package/dist-lib/spec/calls.js +1 -0
  35. package/dist-lib/spec/hrpc/hrpc.json +13 -0
  36. package/dist-lib/spec/hrpc/index.js +15 -3
  37. package/dist-lib/spec/hrpc/messages.js +206 -10
  38. package/dist-lib/spec/hyperschema/index.js +206 -10
  39. package/dist-lib/spec/hyperschema/schema.json +163 -1
  40. package/dist-lib/spec/hyperschema/types.d.ts +43 -2
  41. package/package.json +8 -2
@@ -69,6 +69,12 @@ export interface LoadedModel {
69
69
  type PoolLike = ReturnType<typeof createModelPool>;
70
70
  export declare function releaseModel(pool: PoolLike, key: string): void;
71
71
  export declare function loadedModels(pool: PoolLike): LoadedModel[];
72
+ export interface ResidentLease {
73
+ model: string;
74
+ modelId: string;
75
+ release: () => void;
76
+ }
77
+ export declare function leaseResidentModel(pool: PoolLike, wanted?: (model: string) => boolean): Promise<ResidentLease | null>;
72
78
  export declare function isResident(pool: PoolLike, key: string): boolean;
73
79
  export declare function leaseCount(pool: PoolLike, key: string): number;
74
80
  export declare function evictIdle(pool: PoolLike): Promise<void>;
@@ -91,6 +91,18 @@ export function loadedModels(pool) {
91
91
  }
92
92
  return out;
93
93
  }
94
+ export async function leaseResidentModel(pool, wanted = () => true) {
95
+ for (const entry of pool.resources.values()) {
96
+ const key = entry.id;
97
+ const pending = entry.value.peek();
98
+ if (!pending || entry.value.loading || !wanted(key.split('\0')[0]))
99
+ continue;
100
+ pool.open(key);
101
+ const loaded = await pending;
102
+ return { model: loaded.model, modelId: loaded.modelId, release: () => releaseModel(pool, key) };
103
+ }
104
+ return null;
105
+ }
94
106
  export function isResident(pool, key) {
95
107
  const handle = entryOf(pool, key)?.value;
96
108
  return handle !== undefined && handle.peek() !== null;
@@ -0,0 +1,34 @@
1
+ export declare const ENGLISH = "en";
2
+ export declare const NMT_TO_ENGLISH: readonly string[];
3
+ export declare const NMT_FROM_ENGLISH: readonly string[];
4
+ export declare const SLM_MODEL = "TranslatePsy-AfriSLM-0.8B-Q4_K_M-imat";
5
+ export declare function isTranslationModel(model: string): boolean;
6
+ export declare const EURONANO_LANGUAGES: readonly string[];
7
+ export declare const SLM_LANGUAGE_CODES: readonly string[];
8
+ export declare function languageLabel(code: string): string | undefined;
9
+ export declare const SLM_LANGUAGE_NAMES: ReadonlyArray<{
10
+ code: string;
11
+ name: string;
12
+ }>;
13
+ export declare function slmLanguage(code: string): string | undefined;
14
+ export interface NmtPlan {
15
+ route: 'nmt';
16
+ from: string;
17
+ to: string;
18
+ model: string;
19
+ pivot?: string;
20
+ srcVocab?: string;
21
+ dstVocab?: string;
22
+ tag?: string;
23
+ }
24
+ export interface SlmPlan {
25
+ route: 'slm';
26
+ from: string;
27
+ to: string;
28
+ model: string;
29
+ }
30
+ export type TranslationPlan = NmtPlan | SlmPlan;
31
+ export declare function sameLanguage(from: string, to: string): boolean;
32
+ export declare function translationPlanFor(requestedFrom: string, requestedTo: string): TranslationPlan | {
33
+ error: string;
34
+ };
@@ -0,0 +1,166 @@
1
+ export const ENGLISH = 'en';
2
+ const TO_ENGLISH = [
3
+ 'ar',
4
+ 'az',
5
+ 'be',
6
+ 'bg',
7
+ 'bn',
8
+ 'bs',
9
+ 'ca',
10
+ 'cs',
11
+ 'da',
12
+ 'de',
13
+ 'el',
14
+ 'es',
15
+ 'et',
16
+ 'fa',
17
+ 'fi',
18
+ 'fr',
19
+ 'gu',
20
+ 'he',
21
+ 'hi',
22
+ 'hr',
23
+ 'hu',
24
+ 'id',
25
+ 'is',
26
+ 'it',
27
+ 'ja',
28
+ 'kn',
29
+ 'ko',
30
+ 'lt',
31
+ 'lv',
32
+ 'ml',
33
+ 'ms',
34
+ 'mt',
35
+ 'nb',
36
+ 'nl',
37
+ 'nn',
38
+ 'no',
39
+ 'pl',
40
+ 'pt',
41
+ 'ro',
42
+ 'ru',
43
+ 'sk',
44
+ 'sl',
45
+ 'sq',
46
+ 'sr',
47
+ 'sv',
48
+ 'ta',
49
+ 'te',
50
+ 'th',
51
+ 'tr',
52
+ 'uk',
53
+ 'vi',
54
+ 'zh'
55
+ ];
56
+ const FROM_ENGLISH = TO_ENGLISH.filter((code) => !['be', 'mt', 'nn'].includes(code));
57
+ export const NMT_TO_ENGLISH = TO_ENGLISH;
58
+ export const NMT_FROM_ENGLISH = FROM_ENGLISH;
59
+ const SLM_LANGUAGES = [
60
+ { iso: 'af', flores: 'afr_Latn', name: 'Afrikaans' },
61
+ { iso: 'am', flores: 'amh_Ethi', name: 'Amharic' },
62
+ { iso: 'ha', flores: 'hau_Latn', name: 'Hausa' },
63
+ { iso: 'ig', flores: 'ibo_Latn', name: 'Igbo' },
64
+ { iso: 'rw', flores: 'kin_Latn', name: 'Kinyarwanda' },
65
+ { iso: 'mg', flores: 'plt_Latn', name: 'Malagasy' },
66
+ { iso: 'ny', flores: 'nya_Latn', name: 'Nyanja' },
67
+ { iso: 'om', flores: 'gaz_Latn', name: 'Oromo' },
68
+ { iso: 'sn', flores: 'sna_Latn', name: 'Shona' },
69
+ { iso: 'so', flores: 'som_Latn', name: 'Somali' },
70
+ { iso: 'st', flores: 'sot_Latn', name: 'Southern Sotho' },
71
+ { iso: 'sw', flores: 'swh_Latn', name: 'Swahili' },
72
+ { iso: 'tn', flores: 'tsn_Latn', name: 'Tswana' },
73
+ { iso: 'xh', flores: 'xho_Latn', name: 'Xhosa' },
74
+ { iso: 'yo', flores: 'yor_Latn', name: 'Yoruba' },
75
+ { iso: 'zu', flores: 'zul_Latn', name: 'Zulu' }
76
+ ];
77
+ export const SLM_MODEL = 'TranslatePsy-AfriSLM-0.8B-Q4_K_M-imat';
78
+ export function isTranslationModel(model) {
79
+ return /(^|[/\\])TranslatePsy-/.test(model);
80
+ }
81
+ const EURONANO = 'https://huggingface.co/qvac/TranslatePsy-EuroNano/resolve/main';
82
+ const EURONANO_SIZE = 'Base';
83
+ export const EURONANO_LANGUAGES = [
84
+ 'cs',
85
+ 'de',
86
+ 'es',
87
+ 'fi',
88
+ 'fr',
89
+ 'it',
90
+ 'nl',
91
+ 'pt',
92
+ 'sv'
93
+ ];
94
+ function euroNanoPack(direction) {
95
+ const dir = `${EURONANO}/${direction}/${EURONANO_SIZE}/intgemm`;
96
+ return { model: `${dir}/model.intgemm.alphas.bin`, vocab: `${dir}/vocab.spm` };
97
+ }
98
+ export const SLM_LANGUAGE_CODES = SLM_LANGUAGES.map((entry) => entry.iso);
99
+ export function languageLabel(code) {
100
+ const wanted = code.toLowerCase();
101
+ return SLM_LANGUAGES.find((entry) => entry.flores.toLowerCase() === wanted || entry.iso === wanted)?.name;
102
+ }
103
+ export const SLM_LANGUAGE_NAMES = SLM_LANGUAGES.map(({ iso, name }) => ({ code: iso, name }));
104
+ export function slmLanguage(code) {
105
+ const wanted = code.toLowerCase();
106
+ return SLM_LANGUAGES.find((entry) => entry.flores.toLowerCase() === wanted || entry.iso === wanted)?.flores;
107
+ }
108
+ const modelFilter = (pair) => `model.${pair}.intgemm`;
109
+ export function sameLanguage(from, to) {
110
+ return canonical(from) === canonical(to);
111
+ }
112
+ function canonical(code) {
113
+ return slmLanguage(code) ?? code.toLowerCase();
114
+ }
115
+ export function translationPlanFor(requestedFrom, requestedTo) {
116
+ const from = requestedFrom.toLowerCase();
117
+ const to = requestedTo.toLowerCase();
118
+ if (sameLanguage(from, to))
119
+ return { error: `${from} is already the target language` };
120
+ const slmFrom = slmLanguage(from);
121
+ const slmTo = slmLanguage(to);
122
+ if (slmFrom || slmTo) {
123
+ return { route: 'slm', from: slmFrom ?? from, to: slmTo ?? to, model: SLM_MODEL };
124
+ }
125
+ if (to === ENGLISH && EURONANO_LANGUAGES.includes(from)) {
126
+ const pack = euroNanoPack('xx-en');
127
+ return {
128
+ route: 'nmt',
129
+ from,
130
+ to,
131
+ model: pack.model,
132
+ srcVocab: pack.vocab,
133
+ dstVocab: pack.vocab
134
+ };
135
+ }
136
+ if (from === ENGLISH && EURONANO_LANGUAGES.includes(to)) {
137
+ const pack = euroNanoPack('en-xx');
138
+ return {
139
+ route: 'nmt',
140
+ from,
141
+ to,
142
+ model: pack.model,
143
+ srcVocab: pack.vocab,
144
+ dstVocab: pack.vocab,
145
+ tag: `##${to.toUpperCase()}`
146
+ };
147
+ }
148
+ if (to === ENGLISH) {
149
+ if (!TO_ENGLISH.includes(from))
150
+ return { error: `cannot translate from ${from}` };
151
+ return { route: 'nmt', from, to, model: modelFilter(`${from}${ENGLISH}`) };
152
+ }
153
+ if (!FROM_ENGLISH.includes(to))
154
+ return { error: `cannot translate to ${to}` };
155
+ if (from === ENGLISH)
156
+ return { route: 'nmt', from, to, model: modelFilter(`${ENGLISH}${to}`) };
157
+ if (!TO_ENGLISH.includes(from))
158
+ return { error: `cannot translate from ${from}` };
159
+ return {
160
+ route: 'nmt',
161
+ from,
162
+ to,
163
+ model: modelFilter(`${from}${ENGLISH}`),
164
+ pivot: modelFilter(`${ENGLISH}${to}`)
165
+ };
166
+ }
@@ -4,6 +4,7 @@ import { settleOnClose } from './rpc-session.js';
4
4
  import { pipeline, Readable } from 'streamx';
5
5
  import { bindApi } from '../spec/bind.js';
6
6
  import HRPC from '../spec/hrpc/index.js';
7
+ import { isCapability } from './capabilities/invoke.js';
7
8
  import { unsupportedKvCacheDeletion } from './capabilities/kv-cache.js';
8
9
  import { errorMessage } from './error-fields.js';
9
10
  import { hlog } from './log.js';
@@ -11,7 +12,7 @@ import { pushQueue } from './queue.js';
11
12
  import { TextCoalescer } from './helpers/text-coalescer.js';
12
13
  import { prepareOpenclCache } from './models/opencl-cache.js';
13
14
  import { stageFile, sweepStaging } from './staging.js';
14
- import { readSetup } from './tools/skills/manifest-parse.js';
15
+ import { readSetup, toWireSetup } from './tools/skills/manifest-parse.js';
15
16
  import { packVectors, toDownloadWire, toWire } from './wire.js';
16
17
  const MODEL_STATUSES = new Set([
17
18
  'loading',
@@ -96,6 +97,15 @@ export function serveHarness(stream, harness) {
96
97
  text: await harness.transcribe({ model: model ?? '', audio: audio ?? Buffer.alloc(0) })
97
98
  };
98
99
  },
100
+ detectLanguage: async ({ text, topK }) => {
101
+ if (!harness.detectLanguage)
102
+ throw new Error('harness does not support detectLanguage');
103
+ const languages = await harness.detectLanguage({
104
+ text: text ?? '',
105
+ topK: topK ?? undefined
106
+ });
107
+ return { languages };
108
+ },
99
109
  speak: async ({ model, text }) => {
100
110
  if (!harness.speak)
101
111
  throw new Error('harness does not support speak');
@@ -159,7 +169,7 @@ export function serveHarness(stream, harness) {
159
169
  });
160
170
  }
161
171
  function toWireSkill(skill) {
162
- const setupV2 = readSetup(skill.setup);
172
+ const setupV2 = toWireSetup(readSetup(skill.setup));
163
173
  const binaries = (skill.requires?.bins ?? []).map((bin) => ({
164
174
  bin,
165
175
  minVersion: skill.requires?.binMinVersions?.[bin]?.min,
@@ -275,6 +285,25 @@ function speakStream(harness) {
275
285
  }
276
286
  };
277
287
  }
288
+ function runFrame(harness, frame, context) {
289
+ if (frame.type !== 'invoke')
290
+ return harness.completion(context);
291
+ if (!harness.invoke)
292
+ throw new Error('harness does not support invoke');
293
+ const capability = frame.capability ?? '';
294
+ if (!isCapability(capability))
295
+ throw new Error(`unknown capability: ${capability}`);
296
+ return harness.invoke({
297
+ capability,
298
+ args: frame.arguments ?? {},
299
+ enhance: frame.enhance === 'auto' ? 'auto' : 'never',
300
+ agent: frame.agent ?? undefined,
301
+ attach: context.attach,
302
+ fetchAttachment: context.fetchAttachment,
303
+ progress: context.progress,
304
+ signal: context.signal
305
+ });
306
+ }
278
307
  function completion(harness) {
279
308
  return async function* (duplex) {
280
309
  const events = pushQueue({ highWaterMark: WIRE_HIGH_WATER_MARK });
@@ -404,7 +433,7 @@ function completion(harness) {
404
433
  waiter.resolve((frame.payload ?? {}));
405
434
  return;
406
435
  }
407
- if (frame.type !== 'start' || started)
436
+ if (started || (frame.type !== 'start' && frame.type !== 'invoke'))
408
437
  return;
409
438
  started = true;
410
439
  const context = {
@@ -523,7 +552,7 @@ function completion(harness) {
523
552
  return events.push({ type, text: merged });
524
553
  }, { cap: TEXT_COALESCE_CAP, holdMs: TEXT_COALESCE_MS });
525
554
  try {
526
- for await (const event of harness.completion(context)) {
555
+ for await (const event of runFrame(harness, frame, context)) {
527
556
  produced++;
528
557
  const wire = toWire(event);
529
558
  if ((wire.type === 'content' || wire.type === 'thinking') && wire.text) {
@@ -1,6 +1,7 @@
1
1
  import ScopeLock from 'scope-lock';
2
2
  import { AbortedError, raceAbort } from '../helpers/race-abort.js';
3
3
  import { DIFFUSION_CATALOG, imageModelFor } from '../models/model-catalog.js';
4
+ import { createLoadStall } from '../helpers/load-stall.js';
4
5
  import { createLoadTracker } from '../models/model-pool.js';
5
6
  import { resolveModelSrc } from '../models/model-source.js';
6
7
  export const MAX_DIM = 1024;
@@ -36,30 +37,6 @@ function diffusionProgressName(set) {
36
37
  return entry?.name ?? set.model;
37
38
  }
38
39
  export const IMAGE_LOAD_STALL_ERROR = 'image model download stalled';
39
- function createLoadStall(ms) {
40
- let timer = null;
41
- let rejectStall = null;
42
- const promise = new Promise((_, reject) => {
43
- rejectStall = reject;
44
- });
45
- const arm = () => {
46
- if (timer !== null)
47
- clearTimeout(timer);
48
- timer = setTimeout(() => {
49
- rejectStall?.(new Error(IMAGE_LOAD_STALL_ERROR));
50
- }, ms);
51
- };
52
- arm();
53
- return {
54
- bump: arm,
55
- clear: () => {
56
- if (timer !== null)
57
- clearTimeout(timer);
58
- timer = null;
59
- },
60
- promise
61
- };
62
- }
63
40
  export function createDiffusionRuntime({ loadSdk, onProgress, onLoaded, onBeforeLoad, loads = createLoadTracker(), stallMs = IMAGE_LOAD_STALL_MS, ...base }) {
64
41
  let loading = null;
65
42
  let residentFingerprint = null;
@@ -116,7 +93,7 @@ export function createDiffusionRuntime({ loadSdk, onProgress, onLoaded, onBefore
116
93
  config.vaeModelSrc = await resolveModelSrc(sdk, set.vaeModel, 'diffusion');
117
94
  }
118
95
  const modelConfig = Object.keys(config).length ? config : undefined;
119
- const stall = createLoadStall(stallMs);
96
+ const stall = createLoadStall(stallMs, IMAGE_LOAD_STALL_ERROR);
120
97
  const pending = sdk.loadModel({
121
98
  modelSrc,
122
99
  modelType: 'diffusion',
@@ -14,7 +14,7 @@ export function createHttpRequestTool({ fetch: fetchImpl, credentialStore, crede
14
14
  return {
15
15
  name: 'http_request',
16
16
  toolset: 'http-request',
17
- description: 'Make an outbound HTTP or HTTPS request and return the response status and body (truncated). Authorization/Cookie headers are stripped before sending. Stored skill credentials are attached automatically for their allow-listed hosts; `auth` is only needed to force a specific credential key.',
17
+ description: 'Make an outbound HTTP or HTTPS request and return the response status and body (truncated) as text. Authorization/Cookie headers are stripped before sending. Stored skill credentials are attached automatically for their allow-listed hosts; `auth` is only needed to force a specific credential key. For APIs and text responses only: a URL that serves a file — PDF, .docx, .xlsx, an image, any binary — comes back as truncated garbage here. Use web_fetch for those; it downloads and attaches the file.',
18
18
  parameters: {
19
19
  type: 'object',
20
20
  properties: {
@@ -10,3 +10,4 @@ export interface ParsedManifest {
10
10
  export declare function parseManifestBlock(block: string, key?: string): ParsedManifest;
11
11
  export declare function readInstall(v: unknown): InstallStep[] | undefined;
12
12
  export declare function readSetup(v: unknown): SkillSetup | undefined;
13
+ export declare function toWireSetup(setup: SkillSetup | undefined): unknown;
@@ -165,6 +165,9 @@ function readRoutes(v) {
165
165
  const steps = strArr(raw.steps);
166
166
  const configSteps = strArr(raw.configSteps);
167
167
  const fields = readFields(raw.fields);
168
+ const oauth = readOAuth(raw.oauth);
169
+ if (raw.kind === 'oauth' && !oauth)
170
+ continue;
168
171
  routes.push({
169
172
  kind: raw.kind,
170
173
  label: raw.label,
@@ -175,11 +178,99 @@ function readRoutes(v) {
175
178
  ...(steps ? { steps } : {}),
176
179
  ...(configSteps ? { configSteps } : {}),
177
180
  ...(fields ? { fields } : {}),
178
- ...(typeof raw.satisfied === 'boolean' ? { satisfied: raw.satisfied } : {})
181
+ ...(typeof raw.satisfied === 'boolean' ? { satisfied: raw.satisfied } : {}),
182
+ ...(oauth ? { oauth } : {})
179
183
  });
180
184
  }
181
185
  return routes.length > 0 ? routes : undefined;
182
186
  }
187
+ const OAUTH_TOKEN_AUTH = new Set(['pkce-only', 'secret-in-body', 'secret-in-basic']);
188
+ const OAUTH_RESERVED_PARAMS = new Set([
189
+ 'client_id',
190
+ 'client_secret',
191
+ 'redirect_uri',
192
+ 'response_type',
193
+ 'state',
194
+ 'scope',
195
+ 'code_challenge',
196
+ 'code_challenge_method'
197
+ ]);
198
+ function isOAuthTokenAuth(v) {
199
+ return typeof v === 'string' && OAUTH_TOKEN_AUTH.has(v);
200
+ }
201
+ function httpsUrl(v) {
202
+ if (typeof v !== 'string' || v.length === 0)
203
+ return undefined;
204
+ try {
205
+ return new URL(v).protocol === 'https:' ? v : undefined;
206
+ }
207
+ catch {
208
+ return undefined;
209
+ }
210
+ }
211
+ function isPort(v) {
212
+ return typeof v === 'number' && Number.isInteger(v) && v > 0 && v < 65536;
213
+ }
214
+ function readAuthParams(v) {
215
+ const raw = Array.isArray(v)
216
+ ? Object.fromEntries(v.flatMap((pair) => isObj(pair) && typeof pair.key === 'string' && typeof pair.value === 'string'
217
+ ? [[pair.key, pair.value]]
218
+ : []))
219
+ : v;
220
+ const record = readStringRecord(raw);
221
+ if (!record)
222
+ return undefined;
223
+ const out = Object.fromEntries(Object.entries(record).filter(([key]) => !OAUTH_RESERVED_PARAMS.has(key)));
224
+ return Object.keys(out).length > 0 ? out : undefined;
225
+ }
226
+ function readOAuth(v) {
227
+ if (!isObj(v))
228
+ return undefined;
229
+ const authUrl = httpsUrl(v.authUrl);
230
+ const tokenUrl = httpsUrl(v.tokenUrl);
231
+ if (typeof v.stateKey !== 'string' ||
232
+ v.stateKey.length === 0 ||
233
+ !authUrl ||
234
+ !tokenUrl ||
235
+ !isOAuthTokenAuth(v.tokenAuth)) {
236
+ return undefined;
237
+ }
238
+ const scopes = v.scopes === undefined || v.scopes === null ? [] : strArr(v.scopes);
239
+ if (!scopes)
240
+ return undefined;
241
+ const extraAuthParams = readAuthParams(v.extraAuthParams);
242
+ const registrationEndpoint = httpsUrl(v.registrationEndpoint);
243
+ return {
244
+ stateKey: v.stateKey,
245
+ authUrl,
246
+ tokenUrl,
247
+ scopes,
248
+ tokenAuth: v.tokenAuth,
249
+ ...(extraAuthParams ? { extraAuthParams } : {}),
250
+ ...(registrationEndpoint ? { registrationEndpoint } : {}),
251
+ ...(isPort(v.port) ? { port: v.port } : {}),
252
+ ...(typeof v.sharedIosSession === 'boolean' ? { sharedIosSession: v.sharedIosSession } : {})
253
+ };
254
+ }
255
+ export function toWireSetup(setup) {
256
+ if (!setup?.routes)
257
+ return setup;
258
+ return {
259
+ ...setup,
260
+ routes: setup.routes.map((route) => route.oauth?.extraAuthParams
261
+ ? {
262
+ ...route,
263
+ oauth: {
264
+ ...route.oauth,
265
+ extraAuthParams: Object.entries(route.oauth.extraAuthParams).map(([key, value]) => ({
266
+ key,
267
+ value
268
+ }))
269
+ }
270
+ }
271
+ : route)
272
+ };
273
+ }
183
274
  function readFields(v) {
184
275
  if (!Array.isArray(v))
185
276
  return undefined;
@@ -29,6 +29,17 @@ export interface SkillSetupField {
29
29
  readonly secret?: boolean;
30
30
  readonly required?: boolean;
31
31
  }
32
+ export interface SkillOAuthConfig {
33
+ readonly stateKey: string;
34
+ readonly authUrl: string;
35
+ readonly tokenUrl: string;
36
+ readonly scopes: readonly string[];
37
+ readonly tokenAuth: 'pkce-only' | 'secret-in-body' | 'secret-in-basic';
38
+ readonly extraAuthParams?: Readonly<Record<string, string>>;
39
+ readonly registrationEndpoint?: string;
40
+ readonly port?: number;
41
+ readonly sharedIosSession?: boolean;
42
+ }
32
43
  export interface SkillSetupRoute {
33
44
  readonly kind: 'oauth' | 'token' | 'install' | 'picker' | 'instructions';
34
45
  readonly label: string;
@@ -40,6 +51,7 @@ export interface SkillSetupRoute {
40
51
  readonly configSteps?: readonly string[];
41
52
  readonly fields?: readonly SkillSetupField[];
42
53
  readonly satisfied?: boolean;
54
+ readonly oauth?: SkillOAuthConfig;
43
55
  }
44
56
  export interface SkillSetup {
45
57
  readonly summary?: string;
@@ -0,0 +1,78 @@
1
+ import type { AbortSignal as BareAbortSignal } from 'bare-abort-controller';
2
+ import type { ToolDefinition } from '../capabilities/completion.ts';
3
+ import type { ModelSrc } from '../models/context.ts';
4
+ import { type NmtPlan } from '../models/nmt-catalog.ts';
5
+ import { type LoadTracker, type TrackedLoad } from '../models/model-pool.ts';
6
+ import { type ModelRegistrySdk } from '../models/model-source.ts';
7
+ interface BergamotConfig {
8
+ engine: 'Bergamot';
9
+ from: string;
10
+ to: string;
11
+ pivotModel?: {
12
+ modelSrc: ModelSrc;
13
+ };
14
+ srcVocabSrc?: string;
15
+ dstVocabSrc?: string;
16
+ }
17
+ export type TranslationSdk = ModelRegistrySdk & {
18
+ loadModel(params: {
19
+ modelSrc: ModelSrc;
20
+ modelType: 'nmtcpp-translation';
21
+ modelConfig: BergamotConfig;
22
+ onProgress?: (progress: Record<string, unknown>) => void;
23
+ }): TrackedLoad;
24
+ unloadModel(params: {
25
+ modelId: string;
26
+ }): Promise<unknown>;
27
+ cancel?(params: {
28
+ requestId: string;
29
+ }): Promise<void>;
30
+ translate(params: {
31
+ modelId: string;
32
+ text: string;
33
+ stream: false;
34
+ modelType: 'nmt';
35
+ }): {
36
+ text: Promise<string>;
37
+ };
38
+ };
39
+ export declare const TRANSLATION_LOAD_STALL_MS: number;
40
+ export declare const TRANSLATION_LOAD_STALL_ERROR = "translation model download stalled";
41
+ export interface TranslationRuntimeOptions {
42
+ loadSdk: () => Promise<TranslationSdk>;
43
+ loads?: LoadTracker;
44
+ onProgress?: (model: string, progress: Record<string, unknown>) => void;
45
+ stallMs?: number;
46
+ }
47
+ export interface TranslationRuntime {
48
+ loadSdk: () => Promise<TranslationSdk>;
49
+ ensureLoaded: (sdk: TranslationSdk, plan: NmtPlan, signal?: BareAbortSignal) => Promise<{
50
+ modelId: string;
51
+ }>;
52
+ unload: () => Promise<void>;
53
+ enter: (signal?: BareAbortSignal) => Promise<void>;
54
+ exit: () => void;
55
+ }
56
+ export declare function createTranslationRuntime({ loadSdk, loads, onProgress, stallMs }: TranslationRuntimeOptions): TranslationRuntime;
57
+ export type SlmTranslateFn = (model: string, params: {
58
+ text: string;
59
+ from: string;
60
+ to: string;
61
+ }, signal?: BareAbortSignal) => Promise<string>;
62
+ export declare function stripLanguageLabel(text: string, ...codes: string[]): string;
63
+ export interface TranslateArgs {
64
+ text: string;
65
+ to: string;
66
+ from?: string;
67
+ }
68
+ export declare function parseTranslateArgs(args: Record<string, unknown>): TranslateArgs | {
69
+ error: string;
70
+ };
71
+ export declare function sourceLanguage(text: string, from?: string): string | {
72
+ error: string;
73
+ };
74
+ export interface TranslateToolOptions {
75
+ slm?: SlmTranslateFn;
76
+ }
77
+ export declare function createTranslateTool(runtime: TranslationRuntime, { slm }?: TranslateToolOptions): ToolDefinition;
78
+ export {};