@tanstack-ai-browser/webgllm 0.0.0-stage → 0.2.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 daveycodez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,122 @@
1
- # Temporary Holding Version
1
+ # @tanstack-ai-browser/webgllm
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A [TanStack AI](https://tanstack.com/ai) adapter for [webgllm](https://webgllm.dev): LFM3 1.7B, LFM2.5, Liquid's vision models (LFM3-VL and LFM2.5-VL) and Qwen3.5 2B running in the browser on WebGL 2, in a Web Worker on an OffscreenCanvas. It runs with or without WebGPU; where there's no WebGPU (Firefox on Android, older iPhones) it still reads prompts on the GPU.
4
+
5
+ ```sh
6
+ npm install @tanstack-ai-browser/webgllm @tanstack/ai
7
+ ```
8
+
9
+ ```ts
10
+ import { chat } from "@tanstack/ai"
11
+ import { webgllmText } from "@tanstack-ai-browser/webgllm"
12
+
13
+ const stream = chat({
14
+ adapter: webgllmText("LiquidAI/LFM3-1.7B-GGUF"),
15
+ messages,
16
+ threadId
17
+ })
18
+ ```
19
+
20
+ It needs WebGL 2 with float render targets (`EXT_color_buffer_float`) and OffscreenCanvas in workers, which current Chrome, Firefox and Safari have; an adapter's `availability()` says `"unavailable"` where they're missing.
21
+
22
+ The package is shaped like `@tanstack/ai-openai`'s text adapter. The root has `webgllmText(model, config?)`, `WebGllmTextAdapter` (with the runtime methods `availability`, `create` and `destroy`) and the types `WebGllmConfig`, `WebGllmModel`, `WebGllmTextModelOptions`, `Availability` and `DownloadProgressCallback`, and the catalog (below).
23
+
24
+ The engine is the [`webgllm`](https://www.npmjs.com/package/webgllm) package, behind OpenAI's Chat Completions API in the page. The adapter is TanStack's Chat Completions adapter (`@tanstack/openai-base`) with the shared `WebGLLM` instance as its client, so messages, tools, structured output and the stream are TanStack's; it adds loading the model, webgllm's reasoning levels and thinking budget, and its options. Tool-call parsing and repair, thinking, guided tool choice and schema-constrained answers are webgllm's own. See [webgllm.dev](https://webgllm.dev) and its README for how the engine works, its memory and speed.
25
+
26
+ ## The adapter
27
+
28
+ `webgllmText(model, config?)` takes:
29
+
30
+ - `contextLength`: most tokens a chat may hold, prompt and reply together; the model's whole context by default (LFM3: 262,144), never more. Smaller takes less memory. Changing it loads the model again.
31
+ - `worker`: a Web Worker of your own to run the model in (see A worker of your own).
32
+
33
+ There are no callbacks of its own: thinking, text, tool calls and structured output all come back as TanStack's standard stream events, so an app can swap this adapter for TanStack's OpenAI or OpenRouter adapter without other changes.
34
+
35
+ What maps to what:
36
+
37
+ | TanStack AI | webgllm request |
38
+ | ------------------------------------------------- | ------------------------------------------------------------------------ |
39
+ | `systemPrompts`, `messages` (tool calls, results, thinking) | `messages` (a failed tool's error as its result) |
40
+ | `tools` (their `inputSchema`) | `tools` (function `parameters`) |
41
+ | `toolChoice` (`auto`, `none`, `required`, a tool) | `tool_choice`; with `none` webgllm offers the model no tools |
42
+ | `outputSchema` | `response_format: { type: "json_schema" }` |
43
+ | `reasoning` level / `budgetTokens` | `reasoning_effort` (`none` or `high`) / `reasoning.max_tokens` |
44
+ | `promptCache: "none"` | `cache_prompt: false` |
45
+ | `modelOptions` | see below |
46
+
47
+ `modelOptions` are webgllm's own request fields, sent as given, as TanStack's adapters do with their provider's fields: OpenAI's names (`max_completion_tokens`, thinking included; `temperature`, `top_p`, `presence_penalty`, `stop`, `seed`, `parallel_tool_calls`, false keeping only the first tool call) and webgllm's own (`top_k`, `min_p`, `repetition_penalty`, and `repair_tool_calls`, false skipping its tool-call repair). `frequency_penalty` isn't supported: anything but 0 throws webgllm's `unsupported-parameter` error. Their type is webgllm's `ChatCompletionCreateParamsBase`, without the fields the adapter writes from `chat()`'s own options (messages, tools, the output schema, streaming, reasoning).
48
+
49
+ ```ts
50
+ chat({ adapter: webgllmText("LiquidAI/LFM3-1.7B-GGUF"), messages, modelOptions: { max_completion_tokens: 1024, temperature: 0.2 } })
51
+ ```
52
+
53
+ Unset sampling fields take the model card's values (webgllm's), and there's no output cap unless you set one. Events come back as TanStack's Chat Completions adapter writes them: reasoning, text and tool call events, and `RUN_FINISHED` with the finish reason and token usage. Timings aren't in it: measure them from the stream, as for any adapter. A model that fails to load ends the run with a `RUN_ERROR` whose `code` is webgllm's (`download-failed`, `storage-full`, `webgl2-unavailable`); other failures are TanStack's `RUN_ERROR`. Aborting the request stops the generation and ends the stream without one.
54
+
55
+ ## A worker of your own
56
+
57
+ webgllm starts a worker for each model it loads. To run the model in one you start (bundled your way, or shared with your own code), write the worker with webgllm's handler and give it to the adapter:
58
+
59
+ ```ts
60
+ // worker.ts
61
+ import { WebGLLMWorkerHandler } from "@tanstack-ai-browser/webgllm"
62
+
63
+ const handler = new WebGLLMWorkerHandler()
64
+ self.onmessage = (event) => handler.onmessage(event)
65
+ ```
66
+
67
+ ```ts
68
+ const worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module" })
69
+ const adapter = webgllmText("LiquidAI/LFM3-1.7B-GGUF", { worker })
70
+ ```
71
+
72
+ `WebGLLMWorkerHandler` is webgllm's own, exported here too. The worker holds one model: adapters given the same worker share it, and loading another model replaces the one there once its running reply ends. `destroy()` frees the model's GPU memory and leaves the worker running. If the worker fails (its script doesn't load, or it crashes), the call waiting on it rejects with webgllm's `worker-failed` error (webgllm 0.3.1 and later). Without a worker, webgllm keeps several models loaded at once, a worker each, up to its memory budget.
73
+
74
+ ## Thinking and structured output
75
+
76
+ `chat({ outputSchema })` sends the schema as given in `response_format: json_schema`, as an OpenAI call would; webgllm constrains the answer to the schema while it's written. With `stream: true` it streams as TanStack's standard events, the raw JSON as `TEXT_MESSAGE_CONTENT` deltas and a `CUSTOM` `structured-output.complete` event with the object; `stream: false` resolves to the validated object.
77
+
78
+ ```ts
79
+ const stream = chat({
80
+ adapter: webgllmText("LiquidAI/LFM3-1.7B-GGUF"),
81
+ messages,
82
+ outputSchema: MoveSchema, // a Standard Schema (Zod, …) or JSON Schema
83
+ stream: true
84
+ })
85
+
86
+ for await (const event of stream) {
87
+ if (event.type === "REASONING_MESSAGE_CONTENT") showThinking(event.delta)
88
+ if (event.type === "TEXT_MESSAGE_CONTENT") showPartialJson(event.delta)
89
+ if (event.type === "CUSTOM" && event.name === "structured-output.complete") play(event.value.object)
90
+ }
91
+ ```
92
+
93
+ A failure is a `RUN_ERROR`, as in a chat. Aborting stops the generation and ends the stream without one.
94
+
95
+ ## Images
96
+
97
+ Models with a vision encoder read images: Qwen3.5 2B (its ONNX build since webgllm 0.2.3, its GGUF build with its projector since 0.3.3), and Liquid's vision models, LFM3-VL 2B and LFM2.5-VL 3B, 1.6B and 450M, each in Liquid's GGUF and ONNX builds (webgllm 0.3.3 and later). The adapter's `inputModalities` is `["text", "image"]` for them, from the catalog's `supports.input`; `undefined` for a model the catalog doesn't list. Send image parts in a user message, as data (base64) or URLs: they go to webgllm as OpenAI's `image_url`, with `detail` (`auto` by default) setting how many pixels it reads. webgllm loads the vision encoder only for a request with an image and frees it after, so text chats cost nothing more. An image to a model without one throws webgllm's `unsupported-parameter` error.
98
+
99
+ ## Models and loading
100
+
101
+ The root has the catalog too, webgllm's own re-exported under the adapter's names: `WEBGLLM_CHAT_MODELS` (webgllm's `WEBGL_TEXT_MODELS`) lists the models webgllm runs and `WEBGLLM_MODEL_META` (its `WEBGL_MODEL_META`) has each one's files, capabilities and sampling. `WEBGLLM_MODEL_REASONING` has each model's reasoning levels in TanStack's shape, for `supportedReasoningLevels()`. Their types are `WebGllmChatModel`, `WebGllmModelMeta` and `WebGllmModelReasoningByName`. LFM3 thinks by default and Qwen3.5 doesn't; `chat({ reasoning })` switches it.
102
+
103
+ `webgllmText` also takes any Hugging Face repo webgllm can resolve (a supported architecture, in ONNX or GGUF), or the URL of a model's `.gguf` file or `.onnx` graph with its tokenizer files in the same folder: webgllm reads its files and sampling from there. `chat({ reasoning })` then sends `reasoning_effort` `"none"` or `"high"`, which webgllm applies where the model's chat template can switch thinking.
104
+
105
+ ```ts
106
+ import { webgllmText } from "@tanstack-ai-browser/webgllm"
107
+
108
+ const adapter = webgllmText("LiquidAI/LFM3-1.7B-GGUF", { contextLength: 8192 })
109
+ await adapter.availability() // "unavailable", "downloadable", "downloading" or "available" (loaded)
110
+ await adapter.create({ onDownloadProgress: (progress) => console.log(progress) }) // ahead of time; else the first chat loads it
111
+ await adapter.destroy() // frees its GPU memory
112
+ ```
113
+
114
+ The methods have the Prompt API's values and meanings. `availability()` is `"unavailable"` where the engine can't run (checked once per page: webgllm's check makes a WebGL context), `"downloading"` while this page downloads the model (however the load started: `create` or an adapter's first chat), `"available"` when the model is loaded or every file it loads is in webgllm's storage (`webgllm.models.downloaded()`, which reads the Origin Private File System only), else `"downloadable"`. It makes no request (no `webgllm.models.retrieve()`, which asks Hugging Face for file sizes) and starts no worker. `create({ onDownloadProgress?, signal }?)` reports the download, 0 to 1, then 1 once the model is ready (at once if it already is); it resolves to the adapter, and rejects on failure (fine to leave unawaited). Aborting `signal` rejects that call at once with an `AbortError` and stops its progress reports; when no other caller (another call, or a chat waiting for the model) still waits on the load, webgllm cancels it and the model is left unloaded (`availability()` is back to `"downloadable"`). The config's `onDownloadProgress` hears a download the first chat starts too. Adapters for the same model share it, so `destroy()` on one unloads it for all of them.
115
+
116
+ ## Memory
117
+
118
+ The page shares one `WebGLLM` instance, which keeps models loaded while they fit its memory budget and unloads the least recently used past it. See webgllm's README for its budget.
119
+
120
+ ## License
121
+
122
+ MIT
@@ -0,0 +1,150 @@
1
+ /**
2
+ * A TanStack AI text adapter for webgllm: LFM3, LFM2.5 and Qwen3.5 on
3
+ * WebGL 2 in a Web Worker, for browsers without WebGPU, reading images
4
+ * where the model has a vision encoder. webgllm answers
5
+ * OpenAI's Chat Completions API in the page, so this is TanStack's Chat
6
+ * Completions adapter with the page's `WebGLLM` instance as its client (or
7
+ * the one for the adapter's own `worker`):
8
+ * messages, tools, structured output and the stream (text, thinking, tool
9
+ * calls) are TanStack's. This file adds what's webgllm's own: loading the
10
+ * model before a call, and its reasoning levels and thinking budget.
11
+ */
12
+ import type { AdapterYieldChunk, ModelReasoning, TextOptions, TokenUsage } from "@tanstack/ai";
13
+ import type { StructuredOutputOptions, StructuredOutputResult } from "@tanstack/ai/adapters";
14
+ import { OpenAIBaseChatCompletionsTextAdapter } from "@tanstack/openai-base";
15
+ import type { OpenAIClientLike } from "@tanstack/openai-base";
16
+ import { WebGLLM } from "webgllm";
17
+ import type { ChatCompletionCreateParamsBase } from "webgllm";
18
+ import type { WebGllmChatModel, WebGllmModelReasoningByName } from "./catalog.js";
19
+ /**
20
+ * Where a model stands in this browser, with the Prompt API's values
21
+ * (`LanguageModel.availability()`).
22
+ */
23
+ export type Availability = "unavailable" | "downloadable" | "downloading" | "available";
24
+ /** A download's progress, from 0 to 1. */
25
+ export type DownloadProgressCallback = (progress: number) => void;
26
+ /** The page's `WebGLLM`. Exported for tests. */
27
+ export declare const webgllm: WebGLLM;
28
+ /**
29
+ * Per-call `modelOptions`: webgllm's own request fields (OpenAI's names, and
30
+ * its `top_k`, `min_p`, `repetition_penalty` and `repair_tool_calls`), sent
31
+ * as given. The fields the adapter writes from `chat()`'s own options
32
+ * (messages, tools, the output schema, streaming, reasoning) are left out.
33
+ */
34
+ export type WebGllmTextModelOptions = Omit<ChatCompletionCreateParamsBase, "model" | "messages" | "stream" | "stream_options" | "tools" | "tool_choice" | "response_format" | "reasoning_effort" | "reasoning">;
35
+ /** Options for `webgllmText`. */
36
+ export type WebGllmConfig = {
37
+ /**
38
+ * Most tokens a chat may hold, prompt and reply together: the model's
39
+ * whole context length by default, never more. A smaller limit takes less
40
+ * memory and refuses longer chats sooner. Changing it loads the model again.
41
+ */
42
+ contextLength?: number;
43
+ /**
44
+ * A Web Worker of your own to run the model in, in place of the ones
45
+ * webgllm starts (webgllm's `worker` option): a module whose `onmessage`
46
+ * goes to webgllm's `WebGLLMWorkerHandler` (exported here too). It holds
47
+ * one model, so adapters given the same worker share it and loading
48
+ * another model replaces the one there; `destroy()` frees the model and
49
+ * leaves the worker running.
50
+ */
51
+ worker?: Worker;
52
+ /**
53
+ * The model's download progress, 0 to 1, when a call (`chat()`, say)
54
+ * starts its download; then 1 once it's ready. To load it ahead of time
55
+ * with progress, see `create`.
56
+ */
57
+ onDownloadProgress?: DownloadProgressCallback;
58
+ };
59
+ /** A model's reasoning levels: a listed model's, none for a listed one that doesn't reason, else the switch. */
60
+ type ResolveReasoning<TModel extends string> = TModel extends keyof WebGllmModelReasoningByName ? WebGllmModelReasoningByName[TModel] : TModel extends WebGllmChatModel ? never : {
61
+ levels: "off" | "high";
62
+ budget: false;
63
+ };
64
+ /** A model the catalog lists, any Hugging Face repo webgllm can resolve, or the URL of a `.gguf` file or `.onnx` graph. */
65
+ export type WebGllmModel = WebGllmChatModel | (string & {});
66
+ type MetadataByModality = {
67
+ text: unknown;
68
+ image: unknown;
69
+ audio: unknown;
70
+ video: unknown;
71
+ document: unknown;
72
+ };
73
+ /** A Chat Completions request, as the base adapter writes it. */
74
+ type CreateParams = Parameters<OpenAIClientLike["chat"]["completions"]["create"]>[0];
75
+ export declare class WebGllmTextAdapter<TModel extends WebGllmModel> extends OpenAIBaseChatCompletionsTextAdapter<TModel, WebGllmTextModelOptions, ReadonlyArray<"text" | "image">, MetadataByModality, ReadonlyArray<string>, ResolveReasoning<TModel>> {
76
+ readonly name = "webgllm";
77
+ readonly api = "webgllm";
78
+ /**
79
+ * What the model reads, from webgllm's catalog: images too for a model
80
+ * with a vision encoder (Qwen3.5's ONNX build). Not known for a model the
81
+ * catalog doesn't list (webgllm's `models.retrieve()` says, once loaded).
82
+ */
83
+ readonly inputModalities: ReadonlyArray<"text" | "image"> | undefined;
84
+ private readonly contextLength;
85
+ private readonly onDownloadProgress;
86
+ /** The `WebGLLM` the model loads in: the page's, or the adapter's own worker's. */
87
+ private readonly engine;
88
+ constructor(config: WebGllmConfig, model: TModel);
89
+ /**
90
+ * Where the model stands in this browser, with the Prompt API's values:
91
+ * "unavailable" where the engine can't run (WebGL 2 with float render
92
+ * targets, in a worker), "downloading" while this page downloads it,
93
+ * "available" when it's loaded or its files are in webgllm's storage,
94
+ * else "downloadable". It reads storage only.
95
+ */
96
+ availability(): Promise<Availability>;
97
+ /**
98
+ * Loads the model for this adapter's `contextLength` (downloading it the
99
+ * first time), with the download's progress, 0 to 1, then 1 once it's
100
+ * ready. Resolves to this adapter; rejects if it can't load (fine to leave
101
+ * unawaited). `chat()` doesn't need it: its first call loads the model.
102
+ */
103
+ create(options?: {
104
+ onDownloadProgress?: DownloadProgressCallback;
105
+ signal?: AbortSignal;
106
+ }): Promise<this>;
107
+ /** Unloads the model, freeing its GPU memory. Its files stay. Never rejects. */
108
+ destroy(): Promise<void>;
109
+ /**
110
+ * Loads the model for this adapter's context length; webgllm returns at
111
+ * once when it's loaded for it, and shares a load between its callers.
112
+ */
113
+ private load;
114
+ chatStream(options: TextOptions<WebGllmTextModelOptions>): AsyncIterable<AdapterYieldChunk>;
115
+ structuredOutputStream(options: StructuredOutputOptions<WebGllmTextModelOptions>): AsyncIterable<AdapterYieldChunk>;
116
+ structuredOutput(options: StructuredOutputOptions<WebGllmTextModelOptions>): Promise<StructuredOutputResult<unknown>>;
117
+ /** The request, with `modelOptions` as given, the thinking budget and the prompt cache. */
118
+ /**
119
+ * The engine's usage, as TanStack converts it, but with a cached count of 0
120
+ * kept, which TanStack's conversion leaves out: 0 means the whole prompt was
121
+ * read, where no count means the engine didn't say.
122
+ */
123
+ protected chatCompletionsTokenUsage(usage: {
124
+ prompt_tokens_details?: {
125
+ cached_tokens?: number;
126
+ } | null;
127
+ } | null | undefined, source?: unknown): TokenUsage | undefined;
128
+ protected mapOptionsToRequest(options: TextOptions): CreateParams & {
129
+ stream: true;
130
+ };
131
+ protected modelReasoning(model: string): ModelReasoning | undefined;
132
+ /** webgllm streams thinking as `reasoning_content`. */
133
+ protected extractReasoning(chunk: unknown): {
134
+ text: string;
135
+ } | undefined;
136
+ /** Schemas as given: webgllm constrains to any JSON Schema, without OpenAI's strict-mode rewrite. */
137
+ protected makeStructuredOutputCompatible(schema: Record<string, any>): Record<string, any>;
138
+ }
139
+ /**
140
+ * A TanStack AI text adapter for a webgllm model on WebGL 2, for browsers
141
+ * without WebGPU (Firefox on Android, for one).
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * chat({ adapter: webgllmText("LiquidAI/LFM3-1.7B-GGUF"), messages, threadId })
146
+ * ```
147
+ */
148
+ export declare function webgllmText<TModel extends WebGllmModel>(model: TModel, config?: WebGllmConfig): WebGllmTextAdapter<TModel>;
149
+ export {};
150
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAE9F,OAAO,KAAK,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAC5F,OAAO,EAAE,oCAAoC,EAAE,MAAM,uBAAuB,CAAA;AAC5E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,KAAK,EAAE,8BAA8B,EAAgB,MAAM,SAAS,CAAA;AAG3E,OAAO,KAAK,EAAE,gBAAgB,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAA;AAEjF;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,cAAc,GAAG,aAAa,GAAG,WAAW,CAAA;AAEvF,0CAA0C;AAC1C,MAAM,MAAM,wBAAwB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;AAoBjE,gDAAgD;AAChD,eAAO,MAAM,OAAO,SAAqB,CAAA;AAqBzC;;;;;GAKG;AACH,MAAM,MAAM,uBAAuB,GAAG,IAAI,CACxC,8BAA8B,EAC5B,OAAO,GACP,UAAU,GACV,QAAQ,GACR,gBAAgB,GAChB,OAAO,GACP,aAAa,GACb,iBAAiB,GACjB,kBAAkB,GAClB,WAAW,CACd,CAAA;AAED,iCAAiC;AACjC,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,wBAAwB,CAAA;CAC9C,CAAA;AAED,gHAAgH;AAChH,KAAK,gBAAgB,CAAC,MAAM,SAAS,MAAM,IAAI,MAAM,SAAS,MAAM,2BAA2B,GAC3F,2BAA2B,CAAC,MAAM,CAAC,GACnC,MAAM,SAAS,gBAAgB,GAC7B,KAAK,GACL;IAAE,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IAAC,MAAM,EAAE,KAAK,CAAA;CAAE,CAAA;AAE/C,2HAA2H;AAC3H,MAAM,MAAM,YAAY,GAAG,gBAAgB,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAO3D,KAAK,kBAAkB,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAA;AAE9G,iEAAiE;AACjE,KAAK,YAAY,GAAG,UAAU,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,aAAa,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAKpF,qBAAa,kBAAkB,CAAC,MAAM,SAAS,YAAY,CAAE,SAAQ,oCAAoC,CACvG,MAAM,EACN,uBAAuB,EACvB,aAAa,CAAC,MAAM,GAAG,OAAO,CAAC,EAC/B,kBAAkB,EAClB,aAAa,CAAC,MAAM,CAAC,EACrB,gBAAgB,CAAC,MAAM,CAAC,CACzB;IACC,SAAkB,IAAI,aAAY;IAClC,SAAkB,GAAG,aAAY;IACjC;;;;OAIG;IACH,SAAkB,eAAe,EAAE,aAAa,CAAC,MAAM,GAAG,OAAO,CAAC,GAAG,SAAS,CAAA;IAE9E,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoB;IAClD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAsC;IACzE,mFAAmF;IACnF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAE/B,YAAY,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,EAQ/C;IAED;;;;;;OAMG;IACG,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC,CAM1C;IAED;;;;;OAKG;IACH,MAAM,CAAC,OAAO,GAAE;QAAE,kBAAkB,CAAC,EAAE,wBAAwB,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAI3G;IAED,gFAAgF;IAChF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAEvB;IAED;;;OAGG;YACW,IAAI;IAeF,UAAU,CAAC,OAAO,EAAE,WAAW,CAAC,uBAAuB,CAAC,GAAG,aAAa,CAAC,iBAAiB,CAAC,CAG1G;IAEe,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,CAAC,uBAAuB,CAAC,GAAG,aAAa,CAAC,iBAAiB,CAAC,CAGlI;IAEc,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,CAAC,uBAAuB,CAAC,GAAG,OAAO,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC,CAGnI;IAED,2FAA2F;IAC3F;;;;OAIG;IACH,UAAmB,yBAAyB,CAC1C,KAAK,EAAE;QAAE,qBAAqB,CAAC,EAAE;YAAE,aAAa,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,GAAG,SAAS,EACvF,MAAM,CAAC,EAAE,OAAO,GACf,UAAU,GAAG,SAAS,CAKxB;IAED,UAAmB,mBAAmB,CAAC,OAAO,EAAE,WAAW,GAAG,YAAY,GAAG;QAAE,MAAM,EAAE,IAAI,CAAA;KAAE,CAQ5F;IAED,UAAmB,cAAc,CAAC,KAAK,EAAE,MAAM,GAC+B,cAAc,GAAG,SAAS,CACvG;IAED,uDAAuD;IACvD,UAAmB,gBAAgB,CAAC,KAAK,EAAE,OAAO;;kBAIjD;IAED,qGAAqG;IACrG,UAAmB,8BAA8B,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,uBAE5E;CACF;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,MAAM,SAAS,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAE1H"}
@@ -0,0 +1,173 @@
1
+ import { resolveReasoning } from "@tanstack/ai/adapter-internals";
2
+ import { OpenAIBaseChatCompletionsTextAdapter } from "@tanstack/openai-base";
3
+ import { WebGLLM } from "webgllm";
4
+ import { modelMeta, WEBGLLM_MODEL_META } from "./catalog.js";
5
+ /** A `WebGLLM` (running in `worker`, if given), and the models downloading in it, from its events. */
6
+ function createEngine(worker) {
7
+ const webgllm = new WebGLLM(worker && { worker });
8
+ const downloading = new Set();
9
+ webgllm.addEventListener("progress", ({ model, phase, cached }) => {
10
+ if (phase === "download" && !cached)
11
+ downloading.add(model);
12
+ });
13
+ for (const type of ["load", "error", "unload"]) {
14
+ webgllm.addEventListener(type, ({ model }) => downloading.delete(model));
15
+ }
16
+ return { webgllm, downloading };
17
+ }
18
+ /** The page's engine, shared by every adapter without a worker of its own. */
19
+ const pageEngine = createEngine();
20
+ /** The page's `WebGLLM`. Exported for tests. */
21
+ export const webgllm = pageEngine.webgllm;
22
+ /** The engines in the callers' own workers: one a worker, as each worker's handler holds one model. */
23
+ const workerEngines = new WeakMap();
24
+ /** An adapter's engine: its own worker's (shared by every adapter given that worker), else the page's. */
25
+ function engineFor(worker) {
26
+ if (!worker)
27
+ return pageEngine;
28
+ let engine = workerEngines.get(worker);
29
+ if (!engine)
30
+ workerEngines.set(worker, (engine = createEngine(worker)));
31
+ return engine;
32
+ }
33
+ let supported;
34
+ /** Whether the engine runs here, checked once: webgllm's check makes a WebGL context. */
35
+ const engineRuns = () => (supported ??= WebGLLM.isSupported());
36
+ /** The download's share of webgllm's load progress (downloading is its first 90%). */
37
+ const downloadShare = ({ progress, loadedBytes, totalBytes }) => totalBytes ? Math.min((loadedBytes ?? 0) / totalBytes, 1) : Math.min(progress / 0.9, 1);
38
+ /** The switch for a model the catalog doesn't list: webgllm's `reasoning_effort`, a hint it ignores where it can't apply. */
39
+ const UNLISTED_REASONING = { levels: { off: "none", high: "high" }, budget: false };
40
+ const listed = (model) => Object.prototype.hasOwnProperty.call(WEBGLLM_MODEL_META, model);
41
+ const signalOf = (options) => options.abortController?.signal ?? options.request?.signal ?? undefined;
42
+ export class WebGllmTextAdapter extends OpenAIBaseChatCompletionsTextAdapter {
43
+ name = "webgllm";
44
+ api = "webgllm";
45
+ /**
46
+ * What the model reads, from webgllm's catalog: images too for a model
47
+ * with a vision encoder (Qwen3.5's ONNX build). Not known for a model the
48
+ * catalog doesn't list (webgllm's `models.retrieve()` says, once loaded).
49
+ */
50
+ inputModalities;
51
+ contextLength;
52
+ onDownloadProgress;
53
+ /** The `WebGLLM` the model loads in: the page's, or the adapter's own worker's. */
54
+ engine;
55
+ constructor(config, model) {
56
+ const engine = engineFor(config.worker);
57
+ // Chat Completions (`chat.completions.create`) is all the adapter calls.
58
+ super(model, "webgllm", engine.webgllm, { strictFallbackWarning: false });
59
+ this.engine = engine;
60
+ this.contextLength = config.contextLength;
61
+ this.onDownloadProgress = config.onDownloadProgress;
62
+ this.inputModalities = listed(model) ? modelMeta(model).supports.input : undefined;
63
+ }
64
+ /**
65
+ * Where the model stands in this browser, with the Prompt API's values:
66
+ * "unavailable" where the engine can't run (WebGL 2 with float render
67
+ * targets, in a worker), "downloading" while this page downloads it,
68
+ * "available" when it's loaded or its files are in webgllm's storage,
69
+ * else "downloadable". It reads storage only.
70
+ */
71
+ async availability() {
72
+ const { webgllm, downloading } = this.engine;
73
+ if (!engineRuns())
74
+ return "unavailable";
75
+ if (downloading.has(this.model))
76
+ return "downloading";
77
+ const stored = await webgllm.models.downloaded(this.model).catch(() => false);
78
+ return stored ? "available" : "downloadable";
79
+ }
80
+ /**
81
+ * Loads the model for this adapter's `contextLength` (downloading it the
82
+ * first time), with the download's progress, 0 to 1, then 1 once it's
83
+ * ready. Resolves to this adapter; rejects if it can't load (fine to leave
84
+ * unawaited). `chat()` doesn't need it: its first call loads the model.
85
+ */
86
+ create(options = {}) {
87
+ const ready = this.load(options.signal, options.onDownloadProgress).then(() => this);
88
+ ready.catch(() => undefined);
89
+ return ready;
90
+ }
91
+ /** Unloads the model, freeing its GPU memory. Its files stay. Never rejects. */
92
+ destroy() {
93
+ return this.engine.webgllm.unloadModel(this.model).catch(() => undefined);
94
+ }
95
+ /**
96
+ * Loads the model for this adapter's context length; webgllm returns at
97
+ * once when it's loaded for it, and shares a load between its callers.
98
+ */
99
+ async load(signal, onDownloadProgress) {
100
+ const report = (progress) => {
101
+ this.onDownloadProgress?.(progress);
102
+ onDownloadProgress?.(progress);
103
+ };
104
+ await this.engine.webgllm.loadModel(this.model, {
105
+ ...(this.contextLength !== undefined && { contextLength: this.contextLength }),
106
+ ...(signal && { signal }),
107
+ onProgress: (progress) => {
108
+ if (progress.phase === "download" && !progress.cached)
109
+ report(downloadShare(progress));
110
+ }
111
+ });
112
+ report(1);
113
+ }
114
+ async *chatStream(options) {
115
+ await this.load(signalOf(options));
116
+ yield* super.chatStream(options);
117
+ }
118
+ async *structuredOutputStream(options) {
119
+ await this.load(signalOf(options.chatOptions));
120
+ yield* super.structuredOutputStream(options);
121
+ }
122
+ async structuredOutput(options) {
123
+ await this.load(signalOf(options.chatOptions));
124
+ return super.structuredOutput(options);
125
+ }
126
+ /** The request, with `modelOptions` as given, the thinking budget and the prompt cache. */
127
+ /**
128
+ * The engine's usage, as TanStack converts it, but with a cached count of 0
129
+ * kept, which TanStack's conversion leaves out: 0 means the whole prompt was
130
+ * read, where no count means the engine didn't say.
131
+ */
132
+ chatCompletionsTokenUsage(usage, source) {
133
+ const result = super.chatCompletionsTokenUsage(usage, source);
134
+ const cachedTokens = usage?.prompt_tokens_details?.cached_tokens;
135
+ if (!result || cachedTokens !== 0)
136
+ return result;
137
+ return { ...result, promptTokensDetails: { ...result.promptTokensDetails, cachedTokens } };
138
+ }
139
+ mapOptionsToRequest(options) {
140
+ const request = super.mapOptionsToRequest(options);
141
+ const resolved = resolveReasoning(options.reasoning, this.modelReasoning(this.model));
142
+ const budget = resolved?.value === "high" ? resolved.budgetTokens : undefined;
143
+ return Object.assign(request, {
144
+ ...(budget !== undefined && { reasoning: { max_tokens: budget } }),
145
+ ...(options.promptCache?.retention === "none" && { cache_prompt: false })
146
+ });
147
+ }
148
+ modelReasoning(model) {
149
+ return (listed(model) ? modelMeta(model).reasoning : UNLISTED_REASONING);
150
+ }
151
+ /** webgllm streams thinking as `reasoning_content`. */
152
+ extractReasoning(chunk) {
153
+ const text = chunk.choices?.[0]?.delta
154
+ ?.reasoning_content;
155
+ return text ? { text } : undefined;
156
+ }
157
+ /** Schemas as given: webgllm constrains to any JSON Schema, without OpenAI's strict-mode rewrite. */
158
+ makeStructuredOutputCompatible(schema) {
159
+ return schema;
160
+ }
161
+ }
162
+ /**
163
+ * A TanStack AI text adapter for a webgllm model on WebGL 2, for browsers
164
+ * without WebGPU (Firefox on Android, for one).
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * chat({ adapter: webgllmText("LiquidAI/LFM3-1.7B-GGUF"), messages, threadId })
169
+ * ```
170
+ */
171
+ export function webgllmText(model, config) {
172
+ return new WebGllmTextAdapter(config ?? {}, model);
173
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The models webgllm runs: webgllm's own catalog, re-exported under this
3
+ * adapter's names, with their reasoning levels in TanStack AI's shape (for
4
+ * `chat({ reasoning })` and `supportedReasoningLevels()`).
5
+ */
6
+ import type { ModelReasoning } from "@tanstack/ai";
7
+ import type { ModelReasoningCapability } from "@tanstack/ai/adapter-internals";
8
+ import type { WebGlModelMeta, WebGlModelReasoning, WebGlTextModel } from "webgllm";
9
+ export { WEBGL_TEXT_MODELS as WEBGLLM_CHAT_MODELS, WEBGL_MODEL_META as WEBGLLM_MODEL_META } from "webgllm";
10
+ export type { WebGlTextModel as WebGllmChatModel, WebGlModelMeta as WebGllmModelMeta } from "webgllm";
11
+ type WebGllmChatModel = WebGlTextModel;
12
+ /**
13
+ * The reasoning levels of each model that thinks, exactly as its catalog
14
+ * entry gives them: LFM3 and Qwen3.5 switch between `off` and `high`, the
15
+ * LFM2.5 Thinking builds always think (`high` only). Models that don't think
16
+ * aren't in it.
17
+ */
18
+ export type WebGllmModelReasoningByName = {
19
+ [K in WebGllmChatModel as WebGlModelReasoning<K> extends undefined ? never : K]: ModelReasoningCapability<NonNullable<WebGlModelReasoning<K>>>;
20
+ };
21
+ /** Reasoning data of each model (`false`: it doesn't think), for `supportedReasoningLevels()`. */
22
+ export declare const WEBGLLM_MODEL_REASONING: Readonly<Record<WebGllmChatModel, ModelReasoning>>;
23
+ /** A model's catalog entry. */
24
+ export declare const modelMeta: (model: WebGllmChatModel) => WebGlModelMeta;
25
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAA;AAE9E,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAGlF,OAAO,EAAE,iBAAiB,IAAI,mBAAmB,EAAE,gBAAgB,IAAI,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAC1G,YAAY,EAAE,cAAc,IAAI,gBAAgB,EAAE,cAAc,IAAI,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAErG,KAAK,gBAAgB,GAAG,cAAc,CAAA;AAEtC;;;;;GAKG;AACH,MAAM,MAAM,2BAA2B,GAAG;KACvC,CAAC,IAAI,gBAAgB,IAAI,mBAAmB,CAAC,CAAC,CAAC,SAAS,SAAS,GAAG,KAAK,GAAG,CAAC,GAAG,wBAAwB,CACvG,WAAW,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CACpC;CACF,CAAA;AAED,kGAAkG;AAClG,eAAO,MAAM,uBAAuB,EAE/B,QAAQ,CAAC,MAAM,CAAC,gBAAgB,EAAE,cAAc,CAAC,CAAC,CAAA;AAEvD,+BAA+B;AAC/B,eAAO,MAAM,SAAS,UAAW,gBAAgB,KAAG,cAAyC,CAAA"}
@@ -0,0 +1,7 @@
1
+ import { WEBGL_MODEL_META, WEBGL_TEXT_MODELS } from "webgllm";
2
+ // webgllm's catalog as it is: the models it runs, and each one's entry by name.
3
+ export { WEBGL_TEXT_MODELS as WEBGLLM_CHAT_MODELS, WEBGL_MODEL_META as WEBGLLM_MODEL_META } from "webgllm";
4
+ /** Reasoning data of each model (`false`: it doesn't think), for `supportedReasoningLevels()`. */
5
+ export const WEBGLLM_MODEL_REASONING = Object.fromEntries(WEBGL_TEXT_MODELS.map((model) => [model, WEBGL_MODEL_META[model].reasoning ?? false]));
6
+ /** A model's catalog entry. */
7
+ export const modelMeta = (model) => WEBGL_MODEL_META[model];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * TanStack AI adapter for webgllm: LFM3, LFM2.5 and Qwen3.5 in the browser
3
+ * on WebGL 2, for browsers without WebGPU (Firefox on Android, for one).
4
+ * The model catalog (model-meta.ts) is exported here too.
5
+ */
6
+ export { webgllmText, WebGllmTextAdapter } from "./adapter.js";
7
+ export type { Availability, DownloadProgressCallback, WebGllmConfig, WebGllmModel, WebGllmTextModelOptions } from "./adapter.js";
8
+ export { WebGLLMWorkerHandler } from "webgllm";
9
+ export { WEBGLLM_CHAT_MODELS, WEBGLLM_MODEL_META, WEBGLLM_MODEL_REASONING } from "./catalog.js";
10
+ export type { WebGllmChatModel, WebGllmModelMeta, WebGllmModelReasoningByName } from "./catalog.js";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAC9D,YAAY,EAAE,YAAY,EAAE,wBAAwB,EAAE,aAAa,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AAEhI,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AAC/F,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * TanStack AI adapter for webgllm: LFM3, LFM2.5 and Qwen3.5 in the browser
3
+ * on WebGL 2, for browsers without WebGPU (Firefox on Android, for one).
4
+ * The model catalog (model-meta.ts) is exported here too.
5
+ */
6
+ export { webgllmText, WebGllmTextAdapter } from "./adapter.js";
7
+ // webgllm's own worker handler, for a worker of your own (`worker` in the adapter's config).
8
+ export { WebGLLMWorkerHandler } from "webgllm";
9
+ export { WEBGLLM_CHAT_MODELS, WEBGLLM_MODEL_META, WEBGLLM_MODEL_REASONING } from "./catalog.js";
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The memory the page's models take, shared by this project's runtimes
3
+ * (Transformers.js, WebGL, wllama), each of which loads models in its own
4
+ * worker. Before a runtime loads a model it claims the memory: other
5
+ * runtimes' least recently used models are unloaded until everything fits
6
+ * the budget. A phone can't hold two 1 GB models (an iPhone Safari tab has
7
+ * 2 GB), and small models still stay loaded side by side.
8
+ *
9
+ * The packages each carry this file and share one registry through
10
+ * globalThis, so they find each other even when bundled apart. Keep the
11
+ * copies identical (packages/{transformers-js,webgl,wllama}/src).
12
+ */
13
+ /**
14
+ * The default budget: 1.5 GB on iOS (a Safari tab gets 2 GB, the page and
15
+ * Safari take the rest), half the device's memory where the browser says
16
+ * (Chrome: up to 8 GB), else 4 GB.
17
+ */
18
+ export declare function defaultMemoryBudget(): number;
19
+ /**
20
+ * Sets how many bytes the page's models may take together, across this
21
+ * project's runtimes, from the next load on; undefined for the default.
22
+ */
23
+ export declare function setMemoryBudget(bytes: number | undefined): void;
24
+ /**
25
+ * Makes room for `owner`'s model of about `bytes`, unloading other runtimes'
26
+ * models, least recently used first, until it fits the budget (models kept
27
+ * loaded stay), then records it in place of `owner`'s previous one.
28
+ * Resolves to false, recording nothing, if `signal` aborted meanwhile.
29
+ */
30
+ export declare function claimMemory(model: {
31
+ owner: string;
32
+ model: string;
33
+ bytes: number;
34
+ keepLoaded: boolean;
35
+ unload: () => unknown;
36
+ /**
37
+ * The load the claim is for: once it's aborted (canceled or replaced),
38
+ * nothing more is unloaded for it and nothing is recorded.
39
+ */
40
+ signal?: AbortSignal;
41
+ }): Promise<boolean>;
42
+ /** Marks `owner`'s model as just used (for least recently used order). */
43
+ export declare function touchMemory(owner: string): void;
44
+ /** Forgets `owner`'s model, once it's unloaded or failed to load. */
45
+ export declare function releaseMemory(owner: string): void;
46
+ //# sourceMappingURL=model-memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-memory.d.ts","sourceRoot":"","sources":["../src/model-memory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AA6BH;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAS5C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,QAExD;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE;IACvC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,OAAO,CAAA;IACnB,MAAM,EAAE,MAAM,OAAO,CAAA;IACrB;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB,GAAG,OAAO,CAAC,OAAO,CAAC,CAkBnB;AAED,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,QAGxC;AAED,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,QAE1C"}