@leuria/prompt-api 0.1.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/LICENSE +202 -0
- package/README.md +33 -0
- package/dist/index.d.ts +231 -0
- package/dist/index.js +639 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @leuria/prompt-api
|
|
2
|
+
|
|
3
|
+
The [Prompt API](https://github.com/webmachinelearning/prompt-api) (`LanguageModel`) where the browser can't run it, served by the visitor's own AI through [Leuria](https://leuria.eu). Write your feature for the web standard once: where the browser has a built-in model that runs, it answers; elsewhere, the visitor's AI does (ChatGPT, Claude, Mistral or a local model), in any browser.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { installPromptAPI } from "@leuria/prompt-api"
|
|
7
|
+
|
|
8
|
+
await installPromptAPI({ app: "My notes" })
|
|
9
|
+
|
|
10
|
+
if ((await LanguageModel.availability()) !== "unavailable") {
|
|
11
|
+
// From a click the first time: the visitor approves your site in Leuria.
|
|
12
|
+
const session = await LanguageModel.create({ initialPrompts: [{ role: "system", content: "You are concise." }] })
|
|
13
|
+
console.log(await session.prompt("Summarize this note: …"))
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`installPromptAPI` keeps the browser's own `LanguageModel` wherever it can run (`when: "unavailable"`, the default). Use `when: "missing"` to step in only where the browser has none, or `createLanguageModel()` to get the class without touching the global.
|
|
18
|
+
|
|
19
|
+
## How the API maps onto Leuria
|
|
20
|
+
|
|
21
|
+
| Prompt API | With Leuria |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `availability()` | `"available"` once the site is connected, `"downloadable"` before (connecting is the "download"), `"unavailable"` without Leuria |
|
|
24
|
+
| `create()` | From a click the first time: Leuria asks the visitor to approve the site. `downloadprogress` reports 0, then 1 |
|
|
25
|
+
| `prompt()`, `promptStreaming()` | One session of the visitor's AI per `LanguageModel`; each prompt sends only what's new |
|
|
26
|
+
| `tools` | Run in your page, called by the visitor's AI |
|
|
27
|
+
| `responseConstraint` | JSON Schema or RegExp, given to the AI and checked; a mismatch goes back to the AI to fix |
|
|
28
|
+
| `append()`, `clone()`, `destroy()`, `signal` | Supported |
|
|
29
|
+
| Images | Supported when the visitor's AI takes them. Audio is not supported |
|
|
30
|
+
| `contextUsage`, `measureContextUsage()` | Estimates (about 4 characters per token) |
|
|
31
|
+
| `topK`, `temperature`, `samplingMode` | Accepted and ignored: the visitor's AI sets its own |
|
|
32
|
+
|
|
33
|
+
Docs: [leuria.dev](https://leuria.dev). Apache-2.0.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { BridgeProviderOptions, Provider, Message, ProviderSession } from '@leuria/client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `responseConstraint`: a JSON Schema or a RegExp. The visitor's AI gets
|
|
5
|
+
* it as an instruction, and the answer is checked here; a mismatch is
|
|
6
|
+
* sent back to the AI to fix, a few times at most.
|
|
7
|
+
*/
|
|
8
|
+
type ResponseConstraint = Record<string, unknown> | RegExp;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Prompt API input (`LanguageModelPrompt`, `initialPrompts`) to Leuria
|
|
12
|
+
* messages. The spec's rules on roles and `prefix` are enforced here, with
|
|
13
|
+
* the exceptions Chrome throws.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
type LanguageModelMessageRole = "system" | "user" | "assistant";
|
|
17
|
+
type LanguageModelMessageType = "text" | "image" | "audio" | "tool-call" | "tool-response";
|
|
18
|
+
interface LanguageModelMessageContent {
|
|
19
|
+
type: LanguageModelMessageType;
|
|
20
|
+
value: unknown;
|
|
21
|
+
}
|
|
22
|
+
interface LanguageModelMessage {
|
|
23
|
+
role: LanguageModelMessageRole;
|
|
24
|
+
content: string | LanguageModelMessageContent[];
|
|
25
|
+
prefix?: boolean;
|
|
26
|
+
}
|
|
27
|
+
type LanguageModelPrompt = string | LanguageModelMessage[];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `LanguageModel`, the Prompt API's interface, over a Leuria provider: by
|
|
31
|
+
* default the visitor's own AI through the Leuria app.
|
|
32
|
+
*
|
|
33
|
+
* How the spec maps onto Leuria:
|
|
34
|
+
* - `availability()`: "available" once the site is connected,
|
|
35
|
+
* "downloadable" while it isn't (connecting is the "download": it needs
|
|
36
|
+
* a click, like a model download), "unavailable" without Leuria.
|
|
37
|
+
* - `create()` from a click connects the site, reporting `downloadprogress`
|
|
38
|
+
* 0 then 1. The AI session itself starts with the first prompt.
|
|
39
|
+
* - Each `LanguageModel` is one session of the visitor's AI, which keeps
|
|
40
|
+
* the context: a prompt sends only what's new.
|
|
41
|
+
* - `tools` run in the page, called by the visitor's AI.
|
|
42
|
+
* - `responseConstraint` is given to the AI as an instruction and checked
|
|
43
|
+
* here; a mismatch goes back to the AI to fix.
|
|
44
|
+
* - Token counts are estimates (about 4 characters per token).
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
type Availability = "unavailable" | "downloadable" | "downloading" | "available";
|
|
48
|
+
type LanguageModelSamplingMode = "most-predictable" | "predictable" | "slightly-predictable" | "balanced" | "slightly-creative" | "creative" | "most-creative";
|
|
49
|
+
interface LanguageModelExpected {
|
|
50
|
+
type: LanguageModelMessageType;
|
|
51
|
+
languages?: string[];
|
|
52
|
+
}
|
|
53
|
+
interface LanguageModelTool {
|
|
54
|
+
name: string;
|
|
55
|
+
description: string;
|
|
56
|
+
/** JSON Schema of the arguments. */
|
|
57
|
+
inputSchema: object;
|
|
58
|
+
/** Called with the arguments object; its result goes back to the model. */
|
|
59
|
+
execute: (...args: any[]) => Promise<string> | string;
|
|
60
|
+
}
|
|
61
|
+
interface LanguageModelCreateCoreOptions {
|
|
62
|
+
/** Accepted and ignored: the visitor's AI sets its own sampling. */
|
|
63
|
+
topK?: number;
|
|
64
|
+
/** Accepted and ignored: the visitor's AI sets its own sampling. */
|
|
65
|
+
temperature?: number;
|
|
66
|
+
/** Accepted and ignored: the visitor's AI sets its own sampling. */
|
|
67
|
+
samplingMode?: LanguageModelSamplingMode;
|
|
68
|
+
expectedInputs?: LanguageModelExpected[];
|
|
69
|
+
expectedOutputs?: LanguageModelExpected[];
|
|
70
|
+
tools?: LanguageModelTool[];
|
|
71
|
+
}
|
|
72
|
+
interface LanguageModelCreateOptions extends LanguageModelCreateCoreOptions {
|
|
73
|
+
signal?: AbortSignal;
|
|
74
|
+
monitor?: (monitor: EventTarget) => void;
|
|
75
|
+
initialPrompts?: LanguageModelMessage[];
|
|
76
|
+
}
|
|
77
|
+
interface LanguageModelPromptOptions {
|
|
78
|
+
responseConstraint?: ResponseConstraint;
|
|
79
|
+
/** Accepted and ignored: the visitor's AI always needs the constraint in its prompt. */
|
|
80
|
+
omitResponseConstraintInput?: boolean;
|
|
81
|
+
signal?: AbortSignal;
|
|
82
|
+
}
|
|
83
|
+
interface LanguageModelAppendOptions {
|
|
84
|
+
signal?: AbortSignal;
|
|
85
|
+
}
|
|
86
|
+
interface LanguageModelCloneOptions {
|
|
87
|
+
signal?: AbortSignal;
|
|
88
|
+
}
|
|
89
|
+
interface PromptAPIOptions extends BridgeProviderOptions {
|
|
90
|
+
/** The AI behind `LanguageModel`. Default: `leuria()` with these options. */
|
|
91
|
+
provider?: Provider;
|
|
92
|
+
/** Instructions for the AI when the page gives no system prompt. */
|
|
93
|
+
system?: string;
|
|
94
|
+
/** Most tool calls per prompt. Default 20. */
|
|
95
|
+
maxSteps?: number;
|
|
96
|
+
/** How many times an answer that misses the `responseConstraint` goes back to the AI. Default 2. */
|
|
97
|
+
retries?: number;
|
|
98
|
+
}
|
|
99
|
+
/** @internal What a `LanguageModel` starts from (its constructor is not public). */
|
|
100
|
+
interface LanguageModelInit {
|
|
101
|
+
options: LanguageModelCreateCoreOptions;
|
|
102
|
+
system: string;
|
|
103
|
+
history: Message[];
|
|
104
|
+
}
|
|
105
|
+
/** Set on Leuria's `LanguageModel`, so code can tell it from the browser's own. */
|
|
106
|
+
declare const LEURIA_MARK: unique symbol;
|
|
107
|
+
declare function createLanguageModel(options?: PromptAPIOptions): {
|
|
108
|
+
new (token: symbol, init: LanguageModelInit): {
|
|
109
|
+
readonly "__#private@#options": LanguageModelCreateCoreOptions;
|
|
110
|
+
readonly "__#private@#system": string;
|
|
111
|
+
readonly "__#private@#tools": Map<string, LanguageModelTool>;
|
|
112
|
+
readonly "__#private@#history": Message[];
|
|
113
|
+
/** Appended messages, sent with the next prompt. */
|
|
114
|
+
"__#private@#pending": Message[];
|
|
115
|
+
"__#private@#session": ProviderSession | null;
|
|
116
|
+
"__#private@#queue": Promise<unknown>;
|
|
117
|
+
"__#private@#destroyed": boolean;
|
|
118
|
+
readonly "__#private@#lifetime": AbortController;
|
|
119
|
+
"__#private@#overflow": ((event: Event) => void) | null;
|
|
120
|
+
get contextUsage(): number;
|
|
121
|
+
get contextWindow(): number;
|
|
122
|
+
get samplingMode(): LanguageModelSamplingMode | null;
|
|
123
|
+
get oncontextoverflow(): ((event: Event) => void) | null;
|
|
124
|
+
set oncontextoverflow(handler: ((event: Event) => void) | null);
|
|
125
|
+
prompt(input: LanguageModelPrompt, options?: LanguageModelPromptOptions): Promise<string>;
|
|
126
|
+
promptStreaming(input: LanguageModelPrompt, options?: LanguageModelPromptOptions): ReadableStream<string>;
|
|
127
|
+
append(input: LanguageModelPrompt, options?: LanguageModelAppendOptions): Promise<void>;
|
|
128
|
+
measureContextUsage(input: LanguageModelPrompt, options?: LanguageModelPromptOptions): Promise<number>;
|
|
129
|
+
clone(options?: LanguageModelCloneOptions): Promise</*elided*/ any>;
|
|
130
|
+
destroy(): void;
|
|
131
|
+
"__#private@#prompt"(input: LanguageModelPrompt, options: LanguageModelPromptOptions, onDelta?: (delta: string) => void): Promise<string>;
|
|
132
|
+
"__#private@#turn"(messages: Message[], prefix: string | undefined, constraint: ResponseConstraint | undefined, signal: AbortSignal, onDelta?: (delta: string) => void): Promise<string>;
|
|
133
|
+
"__#private@#send"(session: ProviderSession, message: Message, signal: AbortSignal, onDelta?: (delta: string) => void): Promise<string>;
|
|
134
|
+
"__#private@#runTool"(name: string, args: Record<string, unknown>): Promise<{
|
|
135
|
+
ok: false;
|
|
136
|
+
error: string;
|
|
137
|
+
result?: undefined;
|
|
138
|
+
} | {
|
|
139
|
+
ok: true;
|
|
140
|
+
result: string;
|
|
141
|
+
error?: undefined;
|
|
142
|
+
}>;
|
|
143
|
+
"__#private@#ensureSession"(): Promise<ProviderSession>;
|
|
144
|
+
"__#private@#dropSession"(): void;
|
|
145
|
+
"__#private@#assertLive"(): void;
|
|
146
|
+
addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean): void;
|
|
147
|
+
dispatchEvent(event: Event): boolean;
|
|
148
|
+
removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void;
|
|
149
|
+
};
|
|
150
|
+
availability(options?: LanguageModelCreateCoreOptions): Promise<Availability>;
|
|
151
|
+
/** Only in extension contexts in the spec; null here. */
|
|
152
|
+
params(): Promise<null>;
|
|
153
|
+
create(options?: LanguageModelCreateOptions): Promise<{
|
|
154
|
+
readonly "__#private@#options": LanguageModelCreateCoreOptions;
|
|
155
|
+
readonly "__#private@#system": string;
|
|
156
|
+
readonly "__#private@#tools": Map<string, LanguageModelTool>;
|
|
157
|
+
readonly "__#private@#history": Message[];
|
|
158
|
+
/** Appended messages, sent with the next prompt. */
|
|
159
|
+
"__#private@#pending": Message[];
|
|
160
|
+
"__#private@#session": ProviderSession | null;
|
|
161
|
+
"__#private@#queue": Promise<unknown>;
|
|
162
|
+
"__#private@#destroyed": boolean;
|
|
163
|
+
readonly "__#private@#lifetime": AbortController;
|
|
164
|
+
"__#private@#overflow": ((event: Event) => void) | null;
|
|
165
|
+
get contextUsage(): number;
|
|
166
|
+
get contextWindow(): number;
|
|
167
|
+
get samplingMode(): LanguageModelSamplingMode | null;
|
|
168
|
+
get oncontextoverflow(): ((event: Event) => void) | null;
|
|
169
|
+
set oncontextoverflow(handler: ((event: Event) => void) | null);
|
|
170
|
+
prompt(input: LanguageModelPrompt, options?: LanguageModelPromptOptions): Promise<string>;
|
|
171
|
+
promptStreaming(input: LanguageModelPrompt, options?: LanguageModelPromptOptions): ReadableStream<string>;
|
|
172
|
+
append(input: LanguageModelPrompt, options?: LanguageModelAppendOptions): Promise<void>;
|
|
173
|
+
measureContextUsage(input: LanguageModelPrompt, options?: LanguageModelPromptOptions): Promise<number>;
|
|
174
|
+
clone(options?: LanguageModelCloneOptions): Promise</*elided*/ any>;
|
|
175
|
+
destroy(): void;
|
|
176
|
+
"__#private@#prompt"(input: LanguageModelPrompt, options: LanguageModelPromptOptions, onDelta?: (delta: string) => void): Promise<string>;
|
|
177
|
+
"__#private@#turn"(messages: Message[], prefix: string | undefined, constraint: ResponseConstraint | undefined, signal: AbortSignal, onDelta?: (delta: string) => void): Promise<string>;
|
|
178
|
+
"__#private@#send"(session: ProviderSession, message: Message, signal: AbortSignal, onDelta?: (delta: string) => void): Promise<string>;
|
|
179
|
+
"__#private@#runTool"(name: string, args: Record<string, unknown>): Promise<{
|
|
180
|
+
ok: false;
|
|
181
|
+
error: string;
|
|
182
|
+
result?: undefined;
|
|
183
|
+
} | {
|
|
184
|
+
ok: true;
|
|
185
|
+
result: string;
|
|
186
|
+
error?: undefined;
|
|
187
|
+
}>;
|
|
188
|
+
"__#private@#ensureSession"(): Promise<ProviderSession>;
|
|
189
|
+
"__#private@#dropSession"(): void;
|
|
190
|
+
"__#private@#assertLive"(): void;
|
|
191
|
+
addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean): void;
|
|
192
|
+
dispatchEvent(event: Event): boolean;
|
|
193
|
+
removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void;
|
|
194
|
+
}>;
|
|
195
|
+
readonly [LEURIA_MARK]: true;
|
|
196
|
+
};
|
|
197
|
+
type LanguageModelConstructor = ReturnType<typeof createLanguageModel>;
|
|
198
|
+
type LanguageModel = InstanceType<LanguageModelConstructor>;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @leuria/prompt-api: the Prompt API (`LanguageModel`) where the browser
|
|
202
|
+
* can't run it, served by the visitor's own AI through Leuria.
|
|
203
|
+
*
|
|
204
|
+
* import { installPromptAPI } from "@leuria/prompt-api"
|
|
205
|
+
*
|
|
206
|
+
* await installPromptAPI({ app: "My notes" })
|
|
207
|
+
* if ((await LanguageModel.availability()) !== "unavailable") {
|
|
208
|
+
* const session = await LanguageModel.create() // from a click, the first time
|
|
209
|
+
* console.log(await session.prompt("Summarize this note: …"))
|
|
210
|
+
* }
|
|
211
|
+
*/
|
|
212
|
+
|
|
213
|
+
interface InstallOptions extends PromptAPIOptions {
|
|
214
|
+
/**
|
|
215
|
+
* When Leuria provides `LanguageModel`:
|
|
216
|
+
* - `"unavailable"` (default): where the browser has none, or has one
|
|
217
|
+
* that can't run on this device. The browser's own model is used
|
|
218
|
+
* wherever it can run, even if it must download first.
|
|
219
|
+
* - `"missing"`: only where the browser has none.
|
|
220
|
+
* - `"always"`: always, in place of the browser's.
|
|
221
|
+
*/
|
|
222
|
+
when?: "unavailable" | "missing" | "always";
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Define `globalThis.LanguageModel` with Leuria, unless the browser's own
|
|
226
|
+
* model serves (see `when`). Resolves with who answers: `"browser"` or
|
|
227
|
+
* `"leuria"`.
|
|
228
|
+
*/
|
|
229
|
+
declare function installPromptAPI(options?: InstallOptions): Promise<"browser" | "leuria">;
|
|
230
|
+
|
|
231
|
+
export { type Availability, type InstallOptions, LEURIA_MARK, type LanguageModel, type LanguageModelAppendOptions, type LanguageModelCloneOptions, type LanguageModelConstructor, type LanguageModelCreateCoreOptions, type LanguageModelCreateOptions, type LanguageModelExpected, type LanguageModelMessage, type LanguageModelMessageContent, type LanguageModelMessageRole, type LanguageModelMessageType, type LanguageModelPrompt, type LanguageModelPromptOptions, type LanguageModelSamplingMode, type LanguageModelTool, type PromptAPIOptions, type ResponseConstraint, createLanguageModel, installPromptAPI };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
// src/language-model.ts
|
|
2
|
+
import {
|
|
3
|
+
leuria,
|
|
4
|
+
messageText as messageText2,
|
|
5
|
+
newId as newId2
|
|
6
|
+
} from "@leuria/client";
|
|
7
|
+
|
|
8
|
+
// src/constraint.ts
|
|
9
|
+
import { extractJson } from "@leuria/client";
|
|
10
|
+
function constraintInstruction(constraint) {
|
|
11
|
+
if (constraint instanceof RegExp) {
|
|
12
|
+
return `Answer with text that matches this regular expression, and nothing else: ${constraint.toString()}`;
|
|
13
|
+
}
|
|
14
|
+
return `Answer with JSON only (no code fences, no other text), matching this JSON Schema:
|
|
15
|
+
${JSON.stringify(constraint)}`;
|
|
16
|
+
}
|
|
17
|
+
function checkAnswer(text, constraint) {
|
|
18
|
+
if (constraint instanceof RegExp) {
|
|
19
|
+
const whole = new RegExp(`^(?:${constraint.source})$`, constraint.flags.replace(/[gy]/g, ""));
|
|
20
|
+
for (const candidate of [text, text.trim(), unfence(text.trim())]) {
|
|
21
|
+
if (whole.test(candidate)) return { ok: true, text: candidate };
|
|
22
|
+
}
|
|
23
|
+
return { ok: false, error: `the answer does not match ${constraint.toString()}` };
|
|
24
|
+
}
|
|
25
|
+
const value = extractJson(text);
|
|
26
|
+
if (value === void 0) return { ok: false, error: "the answer is not JSON" };
|
|
27
|
+
const error = validate(value, constraint, "$");
|
|
28
|
+
return error ? { ok: false, error } : { ok: true, text: JSON.stringify(value) };
|
|
29
|
+
}
|
|
30
|
+
function unfence(text) {
|
|
31
|
+
return /^```\w*\s*([\s\S]*?)\s*```$/.exec(text)?.[1] ?? text.replace(/^"([\s\S]*)"$/, "$1");
|
|
32
|
+
}
|
|
33
|
+
function validate(value, schema, path) {
|
|
34
|
+
if (typeof schema !== "object" || schema === null) return null;
|
|
35
|
+
if ("const" in schema && JSON.stringify(value) !== JSON.stringify(schema.const)) return `${path} must be ${JSON.stringify(schema.const)}`;
|
|
36
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((e) => JSON.stringify(e) === JSON.stringify(value))) {
|
|
37
|
+
return `${path} must be one of ${JSON.stringify(schema.enum)}`;
|
|
38
|
+
}
|
|
39
|
+
for (const key of ["anyOf", "oneOf"]) {
|
|
40
|
+
const options = schema[key];
|
|
41
|
+
if (Array.isArray(options)) {
|
|
42
|
+
const matches = options.filter((s) => validate(value, s, path) === null).length;
|
|
43
|
+
if (matches === 0 || key === "oneOf" && matches > 1) return `${path} does not match ${key}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(schema.allOf)) {
|
|
47
|
+
for (const s of schema.allOf) {
|
|
48
|
+
const error = validate(value, s, path);
|
|
49
|
+
if (error) return error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const types = schema.type === void 0 ? void 0 : [schema.type].flat();
|
|
53
|
+
if (types && !types.some((t) => isType(value, t))) return `${path} must be ${types.join(" or ")}`;
|
|
54
|
+
if (typeof value === "string") {
|
|
55
|
+
if (typeof schema.minLength === "number" && value.length < schema.minLength) return `${path} is too short`;
|
|
56
|
+
if (typeof schema.maxLength === "number" && value.length > schema.maxLength) return `${path} is too long`;
|
|
57
|
+
if (typeof schema.pattern === "string" && !new RegExp(schema.pattern, "u").test(value)) return `${path} must match ${schema.pattern}`;
|
|
58
|
+
}
|
|
59
|
+
if (typeof value === "number") {
|
|
60
|
+
if (typeof schema.minimum === "number" && value < schema.minimum) return `${path} must be at least ${schema.minimum}`;
|
|
61
|
+
if (typeof schema.maximum === "number" && value > schema.maximum) return `${path} must be at most ${schema.maximum}`;
|
|
62
|
+
if (typeof schema.exclusiveMinimum === "number" && value <= schema.exclusiveMinimum) return `${path} must be more than ${schema.exclusiveMinimum}`;
|
|
63
|
+
if (typeof schema.exclusiveMaximum === "number" && value >= schema.exclusiveMaximum) return `${path} must be less than ${schema.exclusiveMaximum}`;
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
if (typeof schema.minItems === "number" && value.length < schema.minItems) return `${path} needs at least ${schema.minItems} items`;
|
|
67
|
+
if (typeof schema.maxItems === "number" && value.length > schema.maxItems) return `${path} has more than ${schema.maxItems} items`;
|
|
68
|
+
if (schema.items && typeof schema.items === "object" && !Array.isArray(schema.items)) {
|
|
69
|
+
for (const [i, item] of value.entries()) {
|
|
70
|
+
const error = validate(item, schema.items, `${path}[${i}]`);
|
|
71
|
+
if (error) return error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (isType(value, "object")) {
|
|
76
|
+
const object = value;
|
|
77
|
+
const properties = schema.properties ?? {};
|
|
78
|
+
for (const key of Array.isArray(schema.required) ? schema.required : []) {
|
|
79
|
+
if (!(key in object)) return `${path}.${key} is required`;
|
|
80
|
+
}
|
|
81
|
+
for (const [key, item] of Object.entries(object)) {
|
|
82
|
+
if (key in properties) {
|
|
83
|
+
const error = validate(item, properties[key], `${path}.${key}`);
|
|
84
|
+
if (error) return error;
|
|
85
|
+
} else if (schema.additionalProperties === false) {
|
|
86
|
+
return `${path}.${key} is not allowed`;
|
|
87
|
+
} else if (typeof schema.additionalProperties === "object" && schema.additionalProperties !== null) {
|
|
88
|
+
const error = validate(item, schema.additionalProperties, `${path}.${key}`);
|
|
89
|
+
if (error) return error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
function isType(value, type) {
|
|
96
|
+
switch (type) {
|
|
97
|
+
case "string":
|
|
98
|
+
return typeof value === "string";
|
|
99
|
+
case "number":
|
|
100
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
101
|
+
case "integer":
|
|
102
|
+
return Number.isInteger(value);
|
|
103
|
+
case "boolean":
|
|
104
|
+
return typeof value === "boolean";
|
|
105
|
+
case "null":
|
|
106
|
+
return value === null;
|
|
107
|
+
case "array":
|
|
108
|
+
return Array.isArray(value);
|
|
109
|
+
case "object":
|
|
110
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
111
|
+
default:
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/content.ts
|
|
117
|
+
import { messageFiles, messageText, newId } from "@leuria/client";
|
|
118
|
+
|
|
119
|
+
// src/errors.ts
|
|
120
|
+
function domError(name, message) {
|
|
121
|
+
if (typeof DOMException !== "undefined") return new DOMException(message, name);
|
|
122
|
+
const error = new Error(message);
|
|
123
|
+
error.name = name;
|
|
124
|
+
return error;
|
|
125
|
+
}
|
|
126
|
+
function abortReason(signal) {
|
|
127
|
+
return signal.reason ?? domError("AbortError", "The operation was aborted.");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/content.ts
|
|
131
|
+
async function convert(input, initial = false) {
|
|
132
|
+
if (typeof input === "string") return { system: "", messages: [userText(input)] };
|
|
133
|
+
if (!Array.isArray(input)) throw new TypeError("The prompt must be a string or an array of messages.");
|
|
134
|
+
const system = [];
|
|
135
|
+
const messages = [];
|
|
136
|
+
let prefix;
|
|
137
|
+
for (const [index, message] of input.entries()) {
|
|
138
|
+
if (message.role === "system") {
|
|
139
|
+
if (!initial) throw domError("NotSupportedError", "System messages can only be given in initialPrompts.");
|
|
140
|
+
if (index !== 0) throw new TypeError("A system message must come first in initialPrompts.");
|
|
141
|
+
} else if (message.role !== "user" && message.role !== "assistant") {
|
|
142
|
+
throw new TypeError(`Unknown role: ${String(message.role)}`);
|
|
143
|
+
}
|
|
144
|
+
if (message.prefix) {
|
|
145
|
+
if (message.role !== "assistant" || index !== input.length - 1 || initial) {
|
|
146
|
+
throw domError("SyntaxError", "Only the last message, from the assistant, can be a prefix.");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const parts = await toParts(message.content);
|
|
150
|
+
if (message.role === "system") {
|
|
151
|
+
system.push(textOf(parts));
|
|
152
|
+
} else if (message.prefix) {
|
|
153
|
+
prefix = textOf(parts);
|
|
154
|
+
} else {
|
|
155
|
+
messages.push({ id: newId(), role: message.role, parts });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { system: system.join("\n\n"), messages, prefix };
|
|
159
|
+
}
|
|
160
|
+
function userText(text) {
|
|
161
|
+
return { id: newId(), role: "user", parts: [{ type: "text", text }] };
|
|
162
|
+
}
|
|
163
|
+
function textOf(parts) {
|
|
164
|
+
return messageText({ id: "", role: "user", parts });
|
|
165
|
+
}
|
|
166
|
+
async function toParts(content) {
|
|
167
|
+
if (typeof content === "string") return [{ type: "text", text: content }];
|
|
168
|
+
if (!Array.isArray(content)) throw new TypeError("A message's content must be a string or an array.");
|
|
169
|
+
const parts = [];
|
|
170
|
+
for (const item of content) {
|
|
171
|
+
switch (item.type) {
|
|
172
|
+
case "text":
|
|
173
|
+
if (typeof item.value !== "string") throw new TypeError("A text content's value must be a string.");
|
|
174
|
+
parts.push({ type: "text", text: item.value });
|
|
175
|
+
break;
|
|
176
|
+
case "image":
|
|
177
|
+
parts.push({ type: "file", ...await imageData(item.value) });
|
|
178
|
+
break;
|
|
179
|
+
case "audio":
|
|
180
|
+
throw domError("NotSupportedError", "Audio input is not supported.");
|
|
181
|
+
case "tool-call":
|
|
182
|
+
case "tool-response":
|
|
183
|
+
parts.push({ type: "text", text: `[${item.type}] ${typeof item.value === "string" ? item.value : JSON.stringify(item.value)}` });
|
|
184
|
+
break;
|
|
185
|
+
default:
|
|
186
|
+
throw new TypeError(`Unknown content type: ${String(item.type)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return parts;
|
|
190
|
+
}
|
|
191
|
+
function asOneTurn(messages) {
|
|
192
|
+
if (messages.length === 1 && messages[0].role === "user") return messages[0];
|
|
193
|
+
const text = messages.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${messageText(m)}`).join("\n\n");
|
|
194
|
+
return { id: newId(), role: "user", parts: [{ type: "text", text }, ...messages.flatMap(messageFiles)] };
|
|
195
|
+
}
|
|
196
|
+
async function imageData(value) {
|
|
197
|
+
if (typeof Blob !== "undefined" && value instanceof Blob) {
|
|
198
|
+
const bytes = new Uint8Array(await value.arrayBuffer());
|
|
199
|
+
return encode(bytes, value.type || sniff(bytes));
|
|
200
|
+
}
|
|
201
|
+
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
|
|
202
|
+
const bytes = value instanceof ArrayBuffer ? new Uint8Array(value) : new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
203
|
+
return encode(bytes, sniff(bytes));
|
|
204
|
+
}
|
|
205
|
+
if (value && typeof value === "object") return encode(await drawToPng(value), "image/png");
|
|
206
|
+
throw new TypeError("An image's value must be an image, a canvas, a Blob or bytes.");
|
|
207
|
+
}
|
|
208
|
+
function encode(bytes, mediaType) {
|
|
209
|
+
if (!mediaType.startsWith("image/")) throw domError("NotSupportedError", "This image format is not supported.");
|
|
210
|
+
let binary = "";
|
|
211
|
+
for (let i = 0; i < bytes.length; i += 32768) binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
212
|
+
return { mediaType, url: `data:${mediaType};base64,${btoa(binary)}` };
|
|
213
|
+
}
|
|
214
|
+
function sniff(bytes) {
|
|
215
|
+
const starts = (...sig) => sig.every((b, i) => bytes[i] === b);
|
|
216
|
+
if (starts(137, 80, 78, 71)) return "image/png";
|
|
217
|
+
if (starts(255, 216, 255)) return "image/jpeg";
|
|
218
|
+
if (starts(71, 73, 70, 56)) return "image/gif";
|
|
219
|
+
if (starts(82, 73, 70, 70) && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) return "image/webp";
|
|
220
|
+
return "application/octet-stream";
|
|
221
|
+
}
|
|
222
|
+
async function drawToPng(source) {
|
|
223
|
+
if (typeof HTMLImageElement !== "undefined" && source instanceof HTMLImageElement && !source.complete) await source.decode();
|
|
224
|
+
const isData = typeof ImageData !== "undefined" && source instanceof ImageData;
|
|
225
|
+
const size = source;
|
|
226
|
+
const width = size.naturalWidth || size.videoWidth || size.displayWidth || size.width || 0;
|
|
227
|
+
const height = size.naturalHeight || size.videoHeight || size.displayHeight || size.height || 0;
|
|
228
|
+
if (!width || !height) throw domError("InvalidStateError", "The image has no size (not loaded yet?).");
|
|
229
|
+
let blob;
|
|
230
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
231
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
232
|
+
const ctx = canvas.getContext("2d");
|
|
233
|
+
if (isData) ctx.putImageData(source, 0, 0);
|
|
234
|
+
else ctx.drawImage(source, 0, 0);
|
|
235
|
+
blob = await canvas.convertToBlob({ type: "image/png" });
|
|
236
|
+
} else if (typeof document !== "undefined") {
|
|
237
|
+
const canvas = document.createElement("canvas");
|
|
238
|
+
canvas.width = width;
|
|
239
|
+
canvas.height = height;
|
|
240
|
+
const ctx = canvas.getContext("2d");
|
|
241
|
+
if (isData) ctx.putImageData(source, 0, 0);
|
|
242
|
+
else ctx.drawImage(source, 0, 0);
|
|
243
|
+
blob = await new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(domError("InvalidStateError", "The image could not be read.")), "image/png"));
|
|
244
|
+
} else {
|
|
245
|
+
throw domError("NotSupportedError", "Images need a browser.");
|
|
246
|
+
}
|
|
247
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/language-model.ts
|
|
251
|
+
var LEURIA_MARK = /* @__PURE__ */ Symbol.for("leuria.prompt-api");
|
|
252
|
+
var DEFAULT_SYSTEM = "You answer requests from a web page. Reply with the answer only, without preamble or follow-up questions. Use only the tools the page gives you, if any.";
|
|
253
|
+
var CONTEXT_WINDOW = 128e3;
|
|
254
|
+
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
255
|
+
function createLanguageModel(options = {}) {
|
|
256
|
+
const { provider: given, system: defaultSystem = DEFAULT_SYSTEM, maxSteps = 20, retries = 2, ...bridgeOptions } = options;
|
|
257
|
+
let shared = given;
|
|
258
|
+
const provider = () => shared ??= leuria(bridgeOptions);
|
|
259
|
+
const key = /* @__PURE__ */ Symbol("LanguageModel");
|
|
260
|
+
const live = /* @__PURE__ */ new Set();
|
|
261
|
+
let watching = false;
|
|
262
|
+
const watchUnload = () => {
|
|
263
|
+
if (watching || typeof window === "undefined") return;
|
|
264
|
+
watching = true;
|
|
265
|
+
window.addEventListener("pagehide", () => {
|
|
266
|
+
for (const model of [...live]) model.destroy();
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
class LanguageModel extends EventTarget {
|
|
270
|
+
static [LEURIA_MARK] = true;
|
|
271
|
+
static async availability(options2 = {}) {
|
|
272
|
+
validateCore(options2);
|
|
273
|
+
return availabilityOf(provider(), options2);
|
|
274
|
+
}
|
|
275
|
+
/** Only in extension contexts in the spec; null here. */
|
|
276
|
+
static async params() {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
static async create(options2 = {}) {
|
|
280
|
+
validateCore(options2);
|
|
281
|
+
const { signal } = options2;
|
|
282
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
283
|
+
const initial = await convert(options2.initialPrompts ?? [], true);
|
|
284
|
+
const ai = provider();
|
|
285
|
+
let availability = await availabilityOf(ai, options2);
|
|
286
|
+
if (availability === "unavailable") throw domError("NotSupportedError", "No AI can serve these options here.");
|
|
287
|
+
const monitor = new EventTarget();
|
|
288
|
+
options2.monitor?.(monitor);
|
|
289
|
+
if (availability !== "available") {
|
|
290
|
+
if (!hasActivation()) {
|
|
291
|
+
throw domError("NotAllowedError", "Connecting the visitor's AI needs a user gesture: call create() from a click.");
|
|
292
|
+
}
|
|
293
|
+
if (!ai.connect) throw domError("NotSupportedError", "This AI cannot be connected from the page.");
|
|
294
|
+
progress(monitor, 0);
|
|
295
|
+
await untilAborted(ai.connect(), signal);
|
|
296
|
+
availability = await availabilityOf(ai, options2);
|
|
297
|
+
if (availability !== "available") throw domError("NotAllowedError", "The visitor's AI was not connected.");
|
|
298
|
+
progress(monitor, 1);
|
|
299
|
+
}
|
|
300
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
301
|
+
const { signal: _signal, monitor: _monitor, initialPrompts: _initial, ...core } = options2;
|
|
302
|
+
return new LanguageModel(key, { options: core, system: initial.system || defaultSystem, history: initial.messages });
|
|
303
|
+
}
|
|
304
|
+
#options;
|
|
305
|
+
#system;
|
|
306
|
+
#tools;
|
|
307
|
+
#history;
|
|
308
|
+
/** Appended messages, sent with the next prompt. */
|
|
309
|
+
#pending = [];
|
|
310
|
+
#session = null;
|
|
311
|
+
#queue = Promise.resolve();
|
|
312
|
+
#destroyed = false;
|
|
313
|
+
#lifetime = new AbortController();
|
|
314
|
+
#overflow = null;
|
|
315
|
+
constructor(token, init) {
|
|
316
|
+
if (token !== key) throw new TypeError("Illegal constructor");
|
|
317
|
+
super();
|
|
318
|
+
this.#options = init.options;
|
|
319
|
+
this.#system = init.system;
|
|
320
|
+
this.#history = [...init.history];
|
|
321
|
+
this.#tools = new Map((init.options.tools ?? []).map((tool) => [tool.name, tool]));
|
|
322
|
+
live.add(this);
|
|
323
|
+
watchUnload();
|
|
324
|
+
}
|
|
325
|
+
get contextUsage() {
|
|
326
|
+
return estimateTokens([this.#system, ...[...this.#history, ...this.#pending].map(messageText2)].join("\n"));
|
|
327
|
+
}
|
|
328
|
+
get contextWindow() {
|
|
329
|
+
return CONTEXT_WINDOW;
|
|
330
|
+
}
|
|
331
|
+
get samplingMode() {
|
|
332
|
+
return this.#options.samplingMode ?? null;
|
|
333
|
+
}
|
|
334
|
+
get oncontextoverflow() {
|
|
335
|
+
return this.#overflow;
|
|
336
|
+
}
|
|
337
|
+
set oncontextoverflow(handler) {
|
|
338
|
+
if (this.#overflow) this.removeEventListener("contextoverflow", this.#overflow);
|
|
339
|
+
this.#overflow = typeof handler === "function" ? handler : null;
|
|
340
|
+
if (this.#overflow) this.addEventListener("contextoverflow", this.#overflow);
|
|
341
|
+
}
|
|
342
|
+
prompt(input, options2 = {}) {
|
|
343
|
+
return this.#prompt(input, options2);
|
|
344
|
+
}
|
|
345
|
+
promptStreaming(input, options2 = {}) {
|
|
346
|
+
const cancel = new AbortController();
|
|
347
|
+
const stream = new ReadableStream({
|
|
348
|
+
start: (controller) => {
|
|
349
|
+
const enqueue = (delta) => {
|
|
350
|
+
try {
|
|
351
|
+
controller.enqueue(delta);
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
this.#prompt(input, { ...options2, signal: anySignal(options2.signal, cancel.signal) }, enqueue).then(
|
|
356
|
+
() => controller.close(),
|
|
357
|
+
(error) => {
|
|
358
|
+
try {
|
|
359
|
+
controller.error(error);
|
|
360
|
+
} catch {
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
},
|
|
365
|
+
cancel: (reason) => cancel.abort(reason ?? domError("AbortError", "The stream was cancelled."))
|
|
366
|
+
});
|
|
367
|
+
return asyncIterable(stream);
|
|
368
|
+
}
|
|
369
|
+
async append(input, options2 = {}) {
|
|
370
|
+
this.#assertLive();
|
|
371
|
+
const signal = anySignal(options2.signal, this.#lifetime.signal);
|
|
372
|
+
if (signal.aborted) throw abortReason(signal);
|
|
373
|
+
const { messages, prefix } = await convert(input);
|
|
374
|
+
if (prefix !== void 0) throw domError("SyntaxError", "append() takes no prefix message.");
|
|
375
|
+
const work = this.#queue.then(() => {
|
|
376
|
+
if (signal.aborted) throw abortReason(signal);
|
|
377
|
+
this.#pending.push(...messages);
|
|
378
|
+
});
|
|
379
|
+
this.#queue = work.catch(() => void 0);
|
|
380
|
+
return untilAborted(work, signal);
|
|
381
|
+
}
|
|
382
|
+
async measureContextUsage(input, options2 = {}) {
|
|
383
|
+
this.#assertLive();
|
|
384
|
+
const { messages, prefix } = await convert(input);
|
|
385
|
+
const constraint = options2.responseConstraint ? constraintInstruction(options2.responseConstraint) : "";
|
|
386
|
+
return estimateTokens([...messages.map(messageText2), prefix ?? "", constraint].join("\n"));
|
|
387
|
+
}
|
|
388
|
+
async clone(options2 = {}) {
|
|
389
|
+
this.#assertLive();
|
|
390
|
+
const signal = anySignal(options2.signal, this.#lifetime.signal);
|
|
391
|
+
await untilAborted(this.#queue, signal);
|
|
392
|
+
this.#assertLive();
|
|
393
|
+
return new LanguageModel(key, { options: this.#options, system: this.#system, history: [...this.#history, ...this.#pending] });
|
|
394
|
+
}
|
|
395
|
+
destroy() {
|
|
396
|
+
if (this.#destroyed) return;
|
|
397
|
+
this.#destroyed = true;
|
|
398
|
+
this.#lifetime.abort(domError("AbortError", "The session was destroyed."));
|
|
399
|
+
this.#dropSession();
|
|
400
|
+
live.delete(this);
|
|
401
|
+
}
|
|
402
|
+
// ── Turns ───────────────────────────────────────────────────────
|
|
403
|
+
async #prompt(input, options2, onDelta) {
|
|
404
|
+
this.#assertLive();
|
|
405
|
+
const signal = anySignal(options2.signal, this.#lifetime.signal);
|
|
406
|
+
if (signal.aborted) throw abortReason(signal);
|
|
407
|
+
const constraint = options2.responseConstraint;
|
|
408
|
+
if (constraint !== void 0 && (typeof constraint !== "object" || constraint === null)) {
|
|
409
|
+
throw new TypeError("responseConstraint must be a JSON Schema or a RegExp.");
|
|
410
|
+
}
|
|
411
|
+
const { messages, prefix } = await convert(input);
|
|
412
|
+
const work = this.#queue.then(() => this.#turn(messages, prefix, constraint, signal, onDelta));
|
|
413
|
+
this.#queue = work.catch(() => void 0);
|
|
414
|
+
return untilAborted(work, signal);
|
|
415
|
+
}
|
|
416
|
+
async #turn(messages, prefix, constraint, signal, onDelta) {
|
|
417
|
+
if (signal.aborted) throw abortReason(signal);
|
|
418
|
+
const sending = [...this.#pending, ...messages];
|
|
419
|
+
const user = asOneTurn(sending.length ? sending : [{ id: newId2(), role: "user", parts: [{ type: "text", text: "" }] }]);
|
|
420
|
+
const hasImages = user.parts.some((p) => p.type === "file" && p.mediaType.startsWith("image/"));
|
|
421
|
+
if (hasImages && !provider().getState().capabilities.includes("images")) {
|
|
422
|
+
throw domError("NotSupportedError", "The visitor's AI does not take images.");
|
|
423
|
+
}
|
|
424
|
+
const instructions = [
|
|
425
|
+
constraint ? constraintInstruction(constraint) : void 0,
|
|
426
|
+
prefix !== void 0 ? `Begin your answer with exactly this text, then continue it:
|
|
427
|
+
${prefix}` : void 0
|
|
428
|
+
].filter((s) => Boolean(s));
|
|
429
|
+
const session = await this.#ensureSession();
|
|
430
|
+
let answer;
|
|
431
|
+
if (constraint) {
|
|
432
|
+
let text = await this.#send(session, withInstructions(user, instructions), signal);
|
|
433
|
+
let check = checkAnswer(stripPrefix(text, prefix), constraint);
|
|
434
|
+
for (let attempt = 0; !check.ok && attempt < retries; attempt++) {
|
|
435
|
+
const fix = `That answer is not valid: ${check.error}. ${constraintInstruction(constraint)}`;
|
|
436
|
+
text = await this.#send(session, { id: newId2(), role: "user", parts: [{ type: "text", text: fix }] }, signal);
|
|
437
|
+
check = checkAnswer(stripPrefix(text, prefix), constraint);
|
|
438
|
+
}
|
|
439
|
+
if (!check.ok) throw domError("UnknownError", `The answer did not match the response constraint: ${check.error}.`);
|
|
440
|
+
answer = check.text;
|
|
441
|
+
if (answer) onDelta?.(answer);
|
|
442
|
+
} else {
|
|
443
|
+
const strip = prefixStripper(prefix, onDelta);
|
|
444
|
+
const text = await this.#send(session, withInstructions(user, instructions), signal, strip.push);
|
|
445
|
+
strip.flush();
|
|
446
|
+
answer = stripPrefix(text, prefix);
|
|
447
|
+
}
|
|
448
|
+
this.#pending = [];
|
|
449
|
+
this.#history.push(user, { id: newId2(), role: "assistant", parts: [{ type: "text", text: (prefix ?? "") + answer }] });
|
|
450
|
+
return answer;
|
|
451
|
+
}
|
|
452
|
+
async #send(session, message, signal, onDelta) {
|
|
453
|
+
const context = {
|
|
454
|
+
turnId: newId2("turn"),
|
|
455
|
+
signal,
|
|
456
|
+
text: (delta) => {
|
|
457
|
+
if (delta && !signal.aborted) onDelta?.(delta);
|
|
458
|
+
},
|
|
459
|
+
reasoning: () => void 0,
|
|
460
|
+
status: () => void 0,
|
|
461
|
+
runTool: ({ name, args }) => this.#runTool(name, args)
|
|
462
|
+
};
|
|
463
|
+
try {
|
|
464
|
+
return (await session.send(message, context)).text;
|
|
465
|
+
} catch (error) {
|
|
466
|
+
if (signal.aborted) throw abortReason(signal);
|
|
467
|
+
this.#dropSession();
|
|
468
|
+
throw domError("UnknownError", error instanceof Error ? error.message : String(error));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async #runTool(name, args) {
|
|
472
|
+
const tool = this.#tools.get(name);
|
|
473
|
+
if (!tool) return { ok: false, error: `Unknown tool: ${name}` };
|
|
474
|
+
try {
|
|
475
|
+
const result = await tool.execute(args);
|
|
476
|
+
return { ok: true, result: typeof result === "string" ? result : JSON.stringify(result ?? null) };
|
|
477
|
+
} catch (error) {
|
|
478
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async #ensureSession() {
|
|
482
|
+
if (this.#session && !this.#session.closed) return this.#session;
|
|
483
|
+
this.#dropSession();
|
|
484
|
+
const ai = provider();
|
|
485
|
+
if (ai.getState().status !== "ready") await ai.detect().catch(() => void 0);
|
|
486
|
+
const state = ai.getState();
|
|
487
|
+
if (state.status !== "ready") throw domError("InvalidStateError", "The visitor's AI is not connected anymore.");
|
|
488
|
+
const tools = [...this.#tools.values()].map(({ name, description, inputSchema }) => ({
|
|
489
|
+
name,
|
|
490
|
+
description,
|
|
491
|
+
inputSchema
|
|
492
|
+
}));
|
|
493
|
+
if (tools.length && !state.capabilities.includes("tools")) throw domError("NotSupportedError", "The visitor's AI cannot use tools.");
|
|
494
|
+
this.#session = await ai.createSession({ system: this.#system, tools, history: [...this.#history], maxSteps });
|
|
495
|
+
return this.#session;
|
|
496
|
+
}
|
|
497
|
+
#dropSession() {
|
|
498
|
+
try {
|
|
499
|
+
this.#session?.close();
|
|
500
|
+
} catch {
|
|
501
|
+
}
|
|
502
|
+
this.#session = null;
|
|
503
|
+
}
|
|
504
|
+
#assertLive() {
|
|
505
|
+
if (this.#destroyed) throw domError("InvalidStateError", "The session was destroyed.");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return LanguageModel;
|
|
509
|
+
}
|
|
510
|
+
function validateCore(options) {
|
|
511
|
+
if (options.samplingMode !== void 0 && (options.topK !== void 0 || options.temperature !== void 0)) {
|
|
512
|
+
throw new TypeError("samplingMode cannot be combined with topK or temperature.");
|
|
513
|
+
}
|
|
514
|
+
for (const expected of [...options.expectedInputs ?? [], ...options.expectedOutputs ?? []]) {
|
|
515
|
+
if (expected.languages) Intl.getCanonicalLocales(expected.languages);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
async function availabilityOf(ai, options) {
|
|
519
|
+
const inputs = (options.expectedInputs ?? []).map((e) => e.type);
|
|
520
|
+
const outputs = (options.expectedOutputs ?? []).map((e) => e.type);
|
|
521
|
+
if (inputs.includes("audio") || outputs.some((t) => t !== "text" && t !== "tool-call")) return "unavailable";
|
|
522
|
+
await ai.detect().catch(() => void 0);
|
|
523
|
+
const state = ai.getState();
|
|
524
|
+
const caps = new Set(state.capabilities);
|
|
525
|
+
if (inputs.includes("image") && !caps.has("images")) return "unavailable";
|
|
526
|
+
if (options.tools?.length && !caps.has("tools")) return "unavailable";
|
|
527
|
+
switch (state.status) {
|
|
528
|
+
case "ready":
|
|
529
|
+
return "available";
|
|
530
|
+
case "needs-action":
|
|
531
|
+
return "downloadable";
|
|
532
|
+
case "downloading":
|
|
533
|
+
return "downloading";
|
|
534
|
+
default:
|
|
535
|
+
return "unavailable";
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function hasActivation() {
|
|
539
|
+
const activation = typeof navigator === "undefined" ? void 0 : navigator.userActivation;
|
|
540
|
+
return activation ? activation.isActive : true;
|
|
541
|
+
}
|
|
542
|
+
function progress(target, loaded) {
|
|
543
|
+
const event = typeof ProgressEvent !== "undefined" ? new ProgressEvent("downloadprogress", { loaded, total: 1 }) : Object.assign(new Event("downloadprogress"), { loaded, total: 1 });
|
|
544
|
+
target.dispatchEvent(event);
|
|
545
|
+
}
|
|
546
|
+
function withInstructions(message, instructions) {
|
|
547
|
+
if (!instructions.length) return message;
|
|
548
|
+
const text = [messageText2(message), ...instructions].filter(Boolean).join("\n\n");
|
|
549
|
+
return { ...message, parts: [{ type: "text", text }, ...message.parts.filter((p) => p.type !== "text")] };
|
|
550
|
+
}
|
|
551
|
+
function stripPrefix(text, prefix) {
|
|
552
|
+
if (!prefix) return text;
|
|
553
|
+
const start = text.trimStart();
|
|
554
|
+
return start.startsWith(prefix) ? start.slice(prefix.length) : text;
|
|
555
|
+
}
|
|
556
|
+
function prefixStripper(prefix, onDelta) {
|
|
557
|
+
if (!prefix) return { push: (delta) => onDelta?.(delta), flush: () => void 0 };
|
|
558
|
+
let buffer = "";
|
|
559
|
+
let decided = false;
|
|
560
|
+
return {
|
|
561
|
+
push(delta) {
|
|
562
|
+
if (decided) return onDelta?.(delta);
|
|
563
|
+
buffer += delta;
|
|
564
|
+
const start = buffer.trimStart();
|
|
565
|
+
if (start.length < prefix.length && prefix.startsWith(start)) return;
|
|
566
|
+
decided = true;
|
|
567
|
+
const rest = start.startsWith(prefix) ? start.slice(prefix.length) : buffer;
|
|
568
|
+
if (rest) onDelta?.(rest);
|
|
569
|
+
},
|
|
570
|
+
flush() {
|
|
571
|
+
if (!decided && !prefix.startsWith(buffer.trimStart())) onDelta?.(buffer);
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
function anySignal(...signals) {
|
|
576
|
+
const list = signals.filter((s) => Boolean(s));
|
|
577
|
+
if (list.length === 1) return list[0];
|
|
578
|
+
if (typeof AbortSignal.any === "function") return AbortSignal.any(list);
|
|
579
|
+
const controller = new AbortController();
|
|
580
|
+
for (const signal of list) {
|
|
581
|
+
if (signal.aborted) {
|
|
582
|
+
controller.abort(signal.reason);
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
signal.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
|
|
586
|
+
}
|
|
587
|
+
return controller.signal;
|
|
588
|
+
}
|
|
589
|
+
function untilAborted(promise, signal) {
|
|
590
|
+
if (!signal) return promise;
|
|
591
|
+
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
592
|
+
return new Promise((resolve, reject) => {
|
|
593
|
+
const onAbort = () => reject(abortReason(signal));
|
|
594
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
595
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
function asyncIterable(stream) {
|
|
599
|
+
if (Symbol.asyncIterator in stream) return stream;
|
|
600
|
+
Object.defineProperty(stream, Symbol.asyncIterator, {
|
|
601
|
+
value: async function* () {
|
|
602
|
+
const reader = stream.getReader();
|
|
603
|
+
try {
|
|
604
|
+
for (; ; ) {
|
|
605
|
+
const { value, done } = await reader.read();
|
|
606
|
+
if (done) return;
|
|
607
|
+
yield value;
|
|
608
|
+
}
|
|
609
|
+
} finally {
|
|
610
|
+
reader.releaseLock();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
return stream;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/index.ts
|
|
618
|
+
async function installPromptAPI(options = {}) {
|
|
619
|
+
const { when = "unavailable", ...rest } = options;
|
|
620
|
+
const scope = globalThis;
|
|
621
|
+
const native = scope.LanguageModel;
|
|
622
|
+
if (native && !native[LEURIA_MARK] && when !== "always") {
|
|
623
|
+
if (when === "missing") return "browser";
|
|
624
|
+
const availability = await Promise.resolve(native.availability?.()).catch(() => "unavailable");
|
|
625
|
+
if (availability !== "unavailable") return "browser";
|
|
626
|
+
}
|
|
627
|
+
Object.defineProperty(globalThis, "LanguageModel", {
|
|
628
|
+
value: createLanguageModel(rest),
|
|
629
|
+
writable: true,
|
|
630
|
+
configurable: true,
|
|
631
|
+
enumerable: false
|
|
632
|
+
});
|
|
633
|
+
return "leuria";
|
|
634
|
+
}
|
|
635
|
+
export {
|
|
636
|
+
LEURIA_MARK,
|
|
637
|
+
createLanguageModel,
|
|
638
|
+
installPromptAPI
|
|
639
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@leuria/prompt-api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The Prompt API (LanguageModel) in every browser, served by the visitor's own AI through Leuria when the browser has no built-in model.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/leur-ia/leuria.git",
|
|
10
|
+
"directory": "packages/prompt-api"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://leuria.dev",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"prompt-api",
|
|
15
|
+
"LanguageModel",
|
|
16
|
+
"polyfill",
|
|
17
|
+
"built-in-ai",
|
|
18
|
+
"web-ai"
|
|
19
|
+
],
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"sideEffects": false,
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@leuria/client": "^0.1.4"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.0.0",
|
|
37
|
+
"tsup": "^8.4.0",
|
|
38
|
+
"typescript": "^5.9.0",
|
|
39
|
+
"vitest": "^3.2.0"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup",
|
|
43
|
+
"check": "tsc --noEmit && tsc -p test",
|
|
44
|
+
"test": "vitest run"
|
|
45
|
+
}
|
|
46
|
+
}
|