@theia/ai-huggingface 1.72.0-next.2 → 1.72.0-next.20
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/lib/node/huggingface-language-model.d.ts.map +1 -1
- package/lib/node/huggingface-language-model.js +27 -1
- package/lib/node/huggingface-language-model.js.map +1 -1
- package/lib/node/huggingface-language-model.spec.d.ts +2 -0
- package/lib/node/huggingface-language-model.spec.d.ts.map +1 -0
- package/lib/node/huggingface-language-model.spec.js +89 -0
- package/lib/node/huggingface-language-model.spec.js.map +1 -0
- package/package.json +4 -4
- package/src/node/huggingface-language-model.spec.ts +96 -0
- package/src/node/huggingface-language-model.ts +31 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"huggingface-language-model.d.ts","sourceRoot":"","sources":["../../src/node/huggingface-language-model.ts"],"names":[],"mappings":"AAgBA,OAAO,EACH,aAAa,EACb,oBAAoB,EAEpB,qBAAqB,EAErB,yBAAyB,EAEzB,mBAAmB,EACtB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,eAAO,MAAM,0BAA0B,eAAuC,CAAC;
|
|
1
|
+
{"version":3,"file":"huggingface-language-model.d.ts","sourceRoot":"","sources":["../../src/node/huggingface-language-model.ts"],"names":[],"mappings":"AAgBA,OAAO,EACH,aAAa,EACb,oBAAoB,EAEpB,qBAAqB,EAErB,yBAAyB,EAEzB,mBAAmB,EACtB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAqD/E,qBAAa,gBAAiB,YAAW,aAAa;aAQ9B,EAAE,EAAE,MAAM;IACnB,KAAK,EAAE,MAAM;IACb,MAAM,EAAE,mBAAmB;IAC3B,MAAM,EAAE,MAAM,MAAM,GAAG,SAAS;aACvB,IAAI,CAAC,EAAE,MAAM;aACb,MAAM,CAAC,EAAE,MAAM;aACf,OAAO,CAAC,EAAE,MAAM;aAChB,MAAM,CAAC,EAAE,MAAM;aACf,cAAc,CAAC,EAAE,MAAM;aACvB,eAAe,CAAC,EAAE,MAAM;IACjC,KAAK,CAAC,EAAE,MAAM;IAhBzB;;;;OAIG;gBAEiB,EAAE,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,mBAAmB,EAC3B,MAAM,EAAE,MAAM,MAAM,GAAG,SAAS,EACvB,IAAI,CAAC,EAAE,MAAM,YAAA,EACb,MAAM,CAAC,EAAE,MAAM,YAAA,EACf,OAAO,CAAC,EAAE,MAAM,YAAA,EAChB,MAAM,CAAC,EAAE,MAAM,YAAA,EACf,cAAc,CAAC,EAAE,MAAM,YAAA,EACvB,eAAe,CAAC,EAAE,MAAM,YAAA,EACjC,KAAK,CAAC,EAAE,MAAM,YAAA;IAGnB,OAAO,CAAC,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IASnH,SAAS,CAAC,WAAW,CAAC,OAAO,EAAE,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;cAI7D,yBAAyB,CAAC,WAAW,EAAE,eAAe,EAAE,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,yBAAyB,CAAC;cAgB1H,sBAAsB,CAClC,WAAW,EAAE,eAAe,EAC5B,OAAO,EAAE,oBAAoB,EAC7B,iBAAiB,CAAC,EAAE,iBAAiB,GACtC,OAAO,CAAC,qBAAqB,CAAC;IA6BjC,SAAS,CAAC,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAKtD,OAAO,CAAC,yBAAyB;CAOpC"}
|
|
@@ -33,12 +33,38 @@ function toRole(actor) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
function toChatMessages(messages) {
|
|
36
|
-
|
|
36
|
+
const chatMessages = messages
|
|
37
37
|
.filter(ai_core_1.LanguageModelMessage.isTextMessage)
|
|
38
38
|
.map(message => ({
|
|
39
39
|
role: toRole(message.actor),
|
|
40
40
|
content: message.text
|
|
41
41
|
}));
|
|
42
|
+
return mergeConsecutiveAssistantMessages(chatMessages);
|
|
43
|
+
}
|
|
44
|
+
function mergeConsecutiveAssistantMessages(messages) {
|
|
45
|
+
const result = [];
|
|
46
|
+
for (const message of messages) {
|
|
47
|
+
const previous = result[result.length - 1];
|
|
48
|
+
if (previous?.role === 'assistant' && message.role === 'assistant') {
|
|
49
|
+
const merged = { ...previous, role: 'assistant' };
|
|
50
|
+
const previousContent = previous.content;
|
|
51
|
+
const nextContent = message.content;
|
|
52
|
+
if (previousContent && nextContent) {
|
|
53
|
+
merged.content = `${previousContent}\n${nextContent}`;
|
|
54
|
+
}
|
|
55
|
+
else if (nextContent) {
|
|
56
|
+
merged.content = nextContent;
|
|
57
|
+
}
|
|
58
|
+
else if (previousContent) {
|
|
59
|
+
merged.content = previousContent;
|
|
60
|
+
}
|
|
61
|
+
result[result.length - 1] = merged;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
result.push(message);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
42
68
|
}
|
|
43
69
|
class HuggingFaceModel {
|
|
44
70
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"huggingface-language-model.js","sourceRoot":"","sources":["../../src/node/huggingface-language-model.ts"],"names":[],"mappings":";AAAA,gFAAgF;AAChF,yCAAyC;AACzC,EAAE;AACF,2EAA2E;AAC3E,mEAAmE;AACnE,wCAAwC;AACxC,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,6EAA6E;AAC7E,yDAAyD;AACzD,uDAAuD;AACvD,EAAE;AACF,gFAAgF;AAChF,gFAAgF;;;AAEhF,4CASwB;AAExB,sDAAyD;AACzD,kDAA2D;AAE9C,QAAA,0BAA0B,GAAG,MAAM,CAAC,4BAA4B,CAAC,CAAC;AAE/E,SAAS,MAAM,CAAC,KAAmB;IAC/B,QAAQ,KAAK,EAAE,CAAC;QACZ,KAAK,MAAM;YACP,OAAO,MAAM,CAAC;QAClB,KAAK,IAAI;YACL,OAAO,WAAW,CAAC;QACvB,KAAK,QAAQ;YACT,OAAO,QAAQ,CAAC;QACpB;YACI,OAAO,MAAM,CAAC;IACtB,CAAC;AACL,CAAC;
|
|
1
|
+
{"version":3,"file":"huggingface-language-model.js","sourceRoot":"","sources":["../../src/node/huggingface-language-model.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"huggingface-language-model.spec.d.ts","sourceRoot":"","sources":["../../src/node/huggingface-language-model.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// *****************************************************************************
|
|
3
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
4
|
+
//
|
|
5
|
+
// This program and the accompanying materials are made available under the
|
|
6
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
7
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
8
|
+
//
|
|
9
|
+
// This Source Code may also be made available under the following Secondary
|
|
10
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
11
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
12
|
+
// with the GNU Classpath Exception which is available at
|
|
13
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
14
|
+
//
|
|
15
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
16
|
+
// *****************************************************************************
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
const chai_1 = require("chai");
|
|
19
|
+
const huggingface_language_model_1 = require("./huggingface-language-model");
|
|
20
|
+
/**
|
|
21
|
+
* Tests cover the message-conversion path that runs before the request leaves Theia.
|
|
22
|
+
* We exercise it by passing crafted messages to a stub HF client through the model.
|
|
23
|
+
*/
|
|
24
|
+
class TestableHuggingFaceModel extends huggingface_language_model_1.HuggingFaceModel {
|
|
25
|
+
constructor() {
|
|
26
|
+
super('test-id', 'test-model', { status: 'ready' }, () => 'test-key');
|
|
27
|
+
}
|
|
28
|
+
// Capture the converted messages by overriding the private fetch step.
|
|
29
|
+
// We re-implement the same conversion path used in handleNonStreamingRequest, since
|
|
30
|
+
// toChatMessages and the merge step are module-private.
|
|
31
|
+
async runConvert(messages) {
|
|
32
|
+
// Use a stub client so the request never goes out. We capture the messages it receives.
|
|
33
|
+
const stubClient = {
|
|
34
|
+
chatCompletion: async (params) => {
|
|
35
|
+
this.capturedMessages = params.messages;
|
|
36
|
+
return { choices: [{ message: { content: '' } }] };
|
|
37
|
+
},
|
|
38
|
+
chatCompletionStream: undefined
|
|
39
|
+
};
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
await this.handleNonStreamingRequest(stubClient, { messages });
|
|
42
|
+
return this.capturedMessages;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
describe('HuggingFaceModel - message merging', () => {
|
|
46
|
+
const model = new TestableHuggingFaceModel();
|
|
47
|
+
it('should merge consecutive assistant text messages with a newline separator', async () => {
|
|
48
|
+
const messages = [
|
|
49
|
+
{ actor: 'user', type: 'text', text: 'q' },
|
|
50
|
+
{ actor: 'ai', type: 'text', text: 'part one' },
|
|
51
|
+
{ actor: 'ai', type: 'text', text: 'part two' }
|
|
52
|
+
];
|
|
53
|
+
const result = await model.runConvert(messages);
|
|
54
|
+
(0, chai_1.expect)(result).to.deep.equal([
|
|
55
|
+
{ role: 'user', content: 'q' },
|
|
56
|
+
{ role: 'assistant', content: 'part one\npart two' }
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
it('should leave alternating user/assistant messages unchanged', async () => {
|
|
60
|
+
const messages = [
|
|
61
|
+
{ actor: 'user', type: 'text', text: 'q1' },
|
|
62
|
+
{ actor: 'ai', type: 'text', text: 'a1' },
|
|
63
|
+
{ actor: 'user', type: 'text', text: 'q2' },
|
|
64
|
+
{ actor: 'ai', type: 'text', text: 'a2' }
|
|
65
|
+
];
|
|
66
|
+
const result = await model.runConvert(messages);
|
|
67
|
+
(0, chai_1.expect)(result).to.deep.equal([
|
|
68
|
+
{ role: 'user', content: 'q1' },
|
|
69
|
+
{ role: 'assistant', content: 'a1' },
|
|
70
|
+
{ role: 'user', content: 'q2' },
|
|
71
|
+
{ role: 'assistant', content: 'a2' }
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
it('should reproduce the bug scenario from issue #17104 (consecutive ai messages)', async () => {
|
|
75
|
+
const messages = [
|
|
76
|
+
{ actor: 'user', type: 'text', text: 'first request' },
|
|
77
|
+
{ actor: 'ai', type: 'text', text: 'reasoning' },
|
|
78
|
+
{ actor: 'ai', type: 'text', text: 'final answer' }
|
|
79
|
+
];
|
|
80
|
+
const result = await model.runConvert(messages);
|
|
81
|
+
// Sanity check: no two consecutive assistant messages remain.
|
|
82
|
+
for (let i = 1; i < result.length; i++) {
|
|
83
|
+
(0, chai_1.expect)(result[i - 1].role === 'assistant' && result[i].role === 'assistant').to.equal(false);
|
|
84
|
+
}
|
|
85
|
+
(0, chai_1.expect)(result).to.have.lengthOf(2);
|
|
86
|
+
(0, chai_1.expect)(result[1]).to.deep.equal({ role: 'assistant', content: 'reasoning\nfinal answer' });
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
//# sourceMappingURL=huggingface-language-model.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"huggingface-language-model.spec.js","sourceRoot":"","sources":["../../src/node/huggingface-language-model.spec.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theia/ai-huggingface",
|
|
3
|
-
"version": "1.72.0-next.
|
|
3
|
+
"version": "1.72.0-next.20+60dd3f3d5",
|
|
4
4
|
"description": "Theia - Hugging Face Integration",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@huggingface/inference": "^4.13.15",
|
|
7
|
-
"@theia/ai-core": "1.72.0-next.
|
|
8
|
-
"@theia/core": "1.72.0-next.
|
|
7
|
+
"@theia/ai-core": "1.72.0-next.20+60dd3f3d5",
|
|
8
|
+
"@theia/core": "1.72.0-next.20+60dd3f3d5"
|
|
9
9
|
},
|
|
10
10
|
"publishConfig": {
|
|
11
11
|
"access": "public"
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"nyc": {
|
|
47
47
|
"extends": "../../configs/nyc.json"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "60dd3f3d5ca48aabc06ab532c215257b061f9725"
|
|
50
50
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { expect } from 'chai';
|
|
18
|
+
import { LanguageModelMessage } from '@theia/ai-core';
|
|
19
|
+
import { HuggingFaceModel } from './huggingface-language-model';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Tests cover the message-conversion path that runs before the request leaves Theia.
|
|
23
|
+
* We exercise it by passing crafted messages to a stub HF client through the model.
|
|
24
|
+
*/
|
|
25
|
+
class TestableHuggingFaceModel extends HuggingFaceModel {
|
|
26
|
+
public capturedMessages: Array<{ role: string; content: string }> | undefined;
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
super('test-id', 'test-model', { status: 'ready' }, () => 'test-key');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Capture the converted messages by overriding the private fetch step.
|
|
33
|
+
// We re-implement the same conversion path used in handleNonStreamingRequest, since
|
|
34
|
+
// toChatMessages and the merge step are module-private.
|
|
35
|
+
public async runConvert(messages: LanguageModelMessage[]): Promise<Array<{ role: string; content: string }>> {
|
|
36
|
+
// Use a stub client so the request never goes out. We capture the messages it receives.
|
|
37
|
+
const stubClient = {
|
|
38
|
+
chatCompletion: async (params: { messages: Array<{ role: string; content: string }> }) => {
|
|
39
|
+
this.capturedMessages = params.messages;
|
|
40
|
+
return { choices: [{ message: { content: '' } }] };
|
|
41
|
+
},
|
|
42
|
+
chatCompletionStream: undefined as unknown
|
|
43
|
+
};
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
await (this as any).handleNonStreamingRequest(stubClient, { messages });
|
|
46
|
+
return this.capturedMessages!;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('HuggingFaceModel - message merging', () => {
|
|
51
|
+
const model = new TestableHuggingFaceModel();
|
|
52
|
+
|
|
53
|
+
it('should merge consecutive assistant text messages with a newline separator', async () => {
|
|
54
|
+
const messages: LanguageModelMessage[] = [
|
|
55
|
+
{ actor: 'user', type: 'text', text: 'q' },
|
|
56
|
+
{ actor: 'ai', type: 'text', text: 'part one' },
|
|
57
|
+
{ actor: 'ai', type: 'text', text: 'part two' }
|
|
58
|
+
];
|
|
59
|
+
const result = await model.runConvert(messages);
|
|
60
|
+
expect(result).to.deep.equal([
|
|
61
|
+
{ role: 'user', content: 'q' },
|
|
62
|
+
{ role: 'assistant', content: 'part one\npart two' }
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should leave alternating user/assistant messages unchanged', async () => {
|
|
67
|
+
const messages: LanguageModelMessage[] = [
|
|
68
|
+
{ actor: 'user', type: 'text', text: 'q1' },
|
|
69
|
+
{ actor: 'ai', type: 'text', text: 'a1' },
|
|
70
|
+
{ actor: 'user', type: 'text', text: 'q2' },
|
|
71
|
+
{ actor: 'ai', type: 'text', text: 'a2' }
|
|
72
|
+
];
|
|
73
|
+
const result = await model.runConvert(messages);
|
|
74
|
+
expect(result).to.deep.equal([
|
|
75
|
+
{ role: 'user', content: 'q1' },
|
|
76
|
+
{ role: 'assistant', content: 'a1' },
|
|
77
|
+
{ role: 'user', content: 'q2' },
|
|
78
|
+
{ role: 'assistant', content: 'a2' }
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should reproduce the bug scenario from issue #17104 (consecutive ai messages)', async () => {
|
|
83
|
+
const messages: LanguageModelMessage[] = [
|
|
84
|
+
{ actor: 'user', type: 'text', text: 'first request' },
|
|
85
|
+
{ actor: 'ai', type: 'text', text: 'reasoning' },
|
|
86
|
+
{ actor: 'ai', type: 'text', text: 'final answer' }
|
|
87
|
+
];
|
|
88
|
+
const result = await model.runConvert(messages);
|
|
89
|
+
// Sanity check: no two consecutive assistant messages remain.
|
|
90
|
+
for (let i = 1; i < result.length; i++) {
|
|
91
|
+
expect(result[i - 1].role === 'assistant' && result[i].role === 'assistant').to.equal(false);
|
|
92
|
+
}
|
|
93
|
+
expect(result).to.have.lengthOf(2);
|
|
94
|
+
expect(result[1]).to.deep.equal({ role: 'assistant', content: 'reasoning\nfinal answer' });
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -43,13 +43,42 @@ function toRole(actor: MessageActor): 'user' | 'assistant' | 'system' {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
47
|
+
type HuggingFaceChatMessage = { role: 'user' | 'assistant' | 'system'; content: string };
|
|
48
|
+
|
|
49
|
+
function toChatMessages(messages: LanguageModelMessage[]): HuggingFaceChatMessage[] {
|
|
50
|
+
const chatMessages = messages
|
|
48
51
|
.filter(LanguageModelMessage.isTextMessage)
|
|
49
52
|
.map(message => ({
|
|
50
53
|
role: toRole(message.actor),
|
|
51
54
|
content: message.text
|
|
52
55
|
}));
|
|
56
|
+
return mergeConsecutiveAssistantMessages(chatMessages);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mergeConsecutiveAssistantMessages(messages: HuggingFaceChatMessage[]): HuggingFaceChatMessage[] {
|
|
60
|
+
const result: HuggingFaceChatMessage[] = [];
|
|
61
|
+
for (const message of messages) {
|
|
62
|
+
const previous = result[result.length - 1];
|
|
63
|
+
if (previous?.role === 'assistant' && message.role === 'assistant') {
|
|
64
|
+
const merged: HuggingFaceChatMessage = { ...previous, role: 'assistant' };
|
|
65
|
+
|
|
66
|
+
const previousContent = previous.content;
|
|
67
|
+
const nextContent = message.content;
|
|
68
|
+
if (previousContent && nextContent) {
|
|
69
|
+
merged.content = `${previousContent}\n${nextContent}`;
|
|
70
|
+
} else if (nextContent) {
|
|
71
|
+
merged.content = nextContent;
|
|
72
|
+
} else if (previousContent) {
|
|
73
|
+
merged.content = previousContent;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
result[result.length - 1] = merged;
|
|
77
|
+
} else {
|
|
78
|
+
result.push(message);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
53
82
|
}
|
|
54
83
|
|
|
55
84
|
export class HuggingFaceModel implements LanguageModel {
|