@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.
- package/dist/harness.bundle +13 -11
- package/dist-lib/index.d.ts +26 -1
- package/dist-lib/index.js +94 -3
- package/dist-lib/lib/capabilities/completion.d.ts +2 -0
- package/dist-lib/lib/capabilities/detect-language.d.ts +11 -0
- package/dist-lib/lib/capabilities/detect-language.js +13 -0
- package/dist-lib/lib/capabilities/enhance.d.ts +10 -0
- package/dist-lib/lib/capabilities/enhance.js +29 -0
- package/dist-lib/lib/capabilities/invoke.d.ts +24 -0
- package/dist-lib/lib/capabilities/invoke.js +57 -0
- package/dist-lib/lib/connect.d.ts +4 -1
- package/dist-lib/lib/connect.js +50 -21
- package/dist-lib/lib/contract-types.d.ts +21 -0
- package/dist-lib/lib/helpers/clean-answer.d.ts +1 -0
- package/dist-lib/lib/helpers/clean-answer.js +16 -0
- package/dist-lib/lib/helpers/load-stall.d.ts +6 -0
- package/dist-lib/lib/helpers/load-stall.js +24 -0
- package/dist-lib/lib/hooks/built-in/sandbox.js +3 -1
- package/dist-lib/lib/models/model-pool.d.ts +6 -0
- package/dist-lib/lib/models/model-pool.js +12 -0
- package/dist-lib/lib/models/nmt-catalog.d.ts +34 -0
- package/dist-lib/lib/models/nmt-catalog.js +166 -0
- package/dist-lib/lib/serve.js +33 -4
- package/dist-lib/lib/tools/generate-image.js +2 -25
- package/dist-lib/lib/tools/http-request.js +1 -1
- package/dist-lib/lib/tools/skills/manifest-parse.d.ts +1 -0
- package/dist-lib/lib/tools/skills/manifest-parse.js +92 -1
- package/dist-lib/lib/tools/skills/manifest.d.ts +12 -0
- package/dist-lib/lib/tools/translate.d.ts +78 -0
- package/dist-lib/lib/tools/translate.js +228 -0
- package/dist-lib/lib/tools/web-fetch.js +12 -2
- package/dist-lib/schema/harness.js +53 -3
- package/dist-lib/spec/bind.js +3 -0
- package/dist-lib/spec/calls.js +1 -0
- package/dist-lib/spec/hrpc/hrpc.json +13 -0
- package/dist-lib/spec/hrpc/index.js +15 -3
- package/dist-lib/spec/hrpc/messages.js +206 -10
- package/dist-lib/spec/hyperschema/index.js +206 -10
- package/dist-lib/spec/hyperschema/schema.json +163 -1
- package/dist-lib/spec/hyperschema/types.d.ts +43 -2
- 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
|
+
}
|
package/dist-lib/lib/serve.js
CHANGED
|
@@ -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'
|
|
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
|
|
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 {};
|