assistant-ui-onetag 0.0.0-stage → 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 +21 -0
- package/README.md +115 -2
- package/dist/adapter.d.ts +31 -0
- package/dist/errors.d.ts +17 -0
- package/dist/index.cjs +246 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +217 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +11 -0
- package/dist/runtime.d.ts +13 -0
- package/dist/sse.d.ts +2 -0
- package/package.json +72 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FengYQ
|
|
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,116 @@
|
|
|
1
|
-
#
|
|
1
|
+
# assistant-ui-onetag
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A [OneTag AI](https://onetagai.com) chat model adapter for [assistant-ui](https://www.assistant-ui.com)'s `LocalRuntime`. The model call runs in the browser, so your chat needs no API route, no proxy and no server: a `vite build`, a Next.js static export or a GitHub Pages site is enough.
|
|
4
|
+
|
|
5
|
+
That only works because a OneTag key is designed to be public. It can sit in your page's JavaScript in production. Every key has a hard token cap, and on the paid plan it can be locked to your domain, so a copied key cannot run up a bill or be used from someone else's site.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install assistant-ui-onetag @assistant-ui/react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
"use client";
|
|
13
|
+
import { AssistantRuntimeProvider } from "@assistant-ui/react";
|
|
14
|
+
import { useOneTagRuntime } from "assistant-ui-onetag";
|
|
15
|
+
import { Thread } from "@/components/assistant-ui/elements/thread.aui"; // npx shadcn add https://r.assistant-ui.com/thread.json
|
|
16
|
+
|
|
17
|
+
export function Chat() {
|
|
18
|
+
const runtime = useOneTagRuntime({
|
|
19
|
+
apiKey: "ot_...", // public by design
|
|
20
|
+
system: "You are a helpful assistant.",
|
|
21
|
+
maxTokens: 800,
|
|
22
|
+
});
|
|
23
|
+
return (
|
|
24
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
25
|
+
<Thread />
|
|
26
|
+
</AssistantRuntimeProvider>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or keep your own `useLocalRuntime` call and pass the adapter:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { useLocalRuntime } from "@assistant-ui/react";
|
|
35
|
+
import { createOneTagAdapter } from "assistant-ui-onetag";
|
|
36
|
+
|
|
37
|
+
const OneTag = createOneTagAdapter({ apiKey: "ot_..." });
|
|
38
|
+
const runtime = useLocalRuntime(OneTag, { adapters: { /* history, suggestions, ... */ } });
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Getting a key
|
|
42
|
+
|
|
43
|
+
Enter your email at [onetagai.com/#get-key](https://onetagai.com/#get-key) and the key arrives by mail. No card. There is no way to use OneTag on a live site without that signup.
|
|
44
|
+
|
|
45
|
+
- Free: 1,000,000 free tokens, one time, no monthly reset.
|
|
46
|
+
- Pro: $9.9/month, 100M tokens, and the key can be locked to your domains.
|
|
47
|
+
|
|
48
|
+
The example in [`examples/vite`](examples/vite) uses a shared demo key when it runs on localhost, so `npm run dev` works before you sign up. That key has a small daily budget shared by everyone; for a real site, use your own key.
|
|
49
|
+
|
|
50
|
+
## Options
|
|
51
|
+
|
|
52
|
+
| Option | Default | Notes |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| `apiKey` | (required) | Your OneTag key. |
|
|
55
|
+
| `system` | none | System prompt. A system prompt from the model context (`useAssistantInstructions`) is appended after it. |
|
|
56
|
+
| `maxTokens` | OneTag's 2000 | Reply length cap, up to 10000. `callSettings.maxTokens` in the model context wins. |
|
|
57
|
+
| `temperature` | model default | `callSettings.temperature` in the model context wins. |
|
|
58
|
+
| `ignoreTools` | `false` | See "Tools" below. |
|
|
59
|
+
| `baseUrl` | `https://onetagai.com/v1` | |
|
|
60
|
+
| `runtime` | none | `useOneTagRuntime` only: options passed to `useLocalRuntime` (`adapters`, `initialMessages`, ...). |
|
|
61
|
+
|
|
62
|
+
## What works and what does not
|
|
63
|
+
|
|
64
|
+
Works:
|
|
65
|
+
|
|
66
|
+
- Streaming text replies, Stop, edit, reload and branching (all handled by `LocalRuntime`).
|
|
67
|
+
- Multi-turn history; system messages in the thread and `useAssistantInstructions` become the system prompt.
|
|
68
|
+
- Any static host. The only network call is a `POST` from the browser to `https://onetagai.com/v1/chat/completions`.
|
|
69
|
+
- A reply cut by `maxTokens` ends as `incomplete` with reason `length`, so the UI can show it was cut.
|
|
70
|
+
|
|
71
|
+
Does not work, and fails with a clear message instead of silently:
|
|
72
|
+
|
|
73
|
+
- **Text only.** A message with an attachment, an image, file, audio or data part, or a tool call in its history throws a `OneTagError` with code `unsupported` that names the part. Reasoning and source parts are left out of the request, since they are display data.
|
|
74
|
+
- **Tools.** OneTag cannot call tools. If tools are registered in the model context (`makeAssistantTool`, `useAssistantTool`), the run fails and says which ones, so a tool UI does not just never fire. Pass `ignoreTools: true` to send text without them.
|
|
75
|
+
- **Browser only.** OneTag rejects requests that carry no `Origin` header (`origin_required`). Server-side code, SSR, route handlers, Node scripts and desktop apps without an Origin cannot use the key. That is deliberate: it keeps the public key from working as a backend key. Browser extensions work. In Next.js, use the hook in a client component; `LocalRuntime` already runs the adapter on the client.
|
|
76
|
+
- No OpenAI Responses API, no JSON schema output, one model (`onetag`).
|
|
77
|
+
|
|
78
|
+
## Errors
|
|
79
|
+
|
|
80
|
+
Errors reach the thread as the message's error status, so the default `Thread` shows them in its error box. Each is a `OneTagError` with `code`, `statusCode` and a readable `message` that ends with OneTag's own text.
|
|
81
|
+
|
|
82
|
+
| `code` | When | Message starts with |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| `missing_key` | `apiKey` is empty | No OneTag key set. Get a free key by email at ... |
|
|
85
|
+
| `bad_key` | 401 | This OneTag key is not valid. ... |
|
|
86
|
+
| `domain_not_allowed` | 403, key locked to other domains | This OneTag key is not allowed on this domain. ... |
|
|
87
|
+
| `origin_required` | 403, called without a browser Origin | OneTag answers only from a browser page or browser extension. ... |
|
|
88
|
+
| `quota_exceeded` | 429, token cap reached | This OneTag key has used all of its tokens. ... |
|
|
89
|
+
| `rate_limited` | 429 | Too many requests to OneTag right now. ... |
|
|
90
|
+
| `too_large` | 413 | The conversation is too long for one OneTag request. ... |
|
|
91
|
+
| `paused` | 503 | This OneTag key is paused. ... |
|
|
92
|
+
| `upstream` | 502, or a failure mid-stream | The model failed to answer. Try again. |
|
|
93
|
+
| `unsupported` | attachments, non-text parts, tools | OneTag is text only ... |
|
|
94
|
+
| `network` | fetch failed | Could not reach OneTag. ... |
|
|
95
|
+
|
|
96
|
+
Aborts (the Stop button) are passed through untouched, so the message ends as cancelled, not as an error.
|
|
97
|
+
|
|
98
|
+
## Example
|
|
99
|
+
|
|
100
|
+
[`examples/vite`](examples/vite) is a Vite app with assistant-ui's shadcn `Thread` component and nothing else:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
cd examples/vite
|
|
104
|
+
npm install
|
|
105
|
+
npm run dev # answers on localhost with the demo key
|
|
106
|
+
VITE_ONETAG_KEY=ot_... npm run build # static files in dist/, deploy anywhere
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Tests
|
|
110
|
+
|
|
111
|
+
- `npm test`: unit tests with mocked `fetch` (request shape, SSE parsing, errors, message mapping) and a test that mounts a real `LocalRuntime` through `useOneTagRuntime`.
|
|
112
|
+
- `npm run test:browser`: builds the example, serves the static build on localhost and drives the `Thread` in Chrome against the live endpoint (three real calls). Needs `playwright-core` and Chrome; see the top of `test/browser/run.mjs`.
|
|
113
|
+
|
|
114
|
+
## About
|
|
115
|
+
|
|
116
|
+
I wrote OneTag AI and this adapter. OneTag is a paid service with a free tier. The adapter is MIT licensed. Service docs: [onetagai.com/docs](https://onetagai.com/docs#openai).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ChatModelAdapter } from "@assistant-ui/react";
|
|
2
|
+
export declare const ONETAG_BASE_URL = "https://onetagai.com/v1";
|
|
3
|
+
export type OneTagAdapterOptions = {
|
|
4
|
+
/** Your OneTag key. It is public by design: it can sit in the page's HTML or JS bundle. */
|
|
5
|
+
apiKey: string;
|
|
6
|
+
/** System prompt, sent before any system prompt from the model context (useAssistantInstructions). */
|
|
7
|
+
system?: string;
|
|
8
|
+
/** Reply length cap. OneTag defaults to 2000 and caps at 10000. A model context callSettings.maxTokens wins. */
|
|
9
|
+
maxTokens?: number;
|
|
10
|
+
/** Sampling temperature. A model context callSettings.temperature wins. */
|
|
11
|
+
temperature?: number;
|
|
12
|
+
/**
|
|
13
|
+
* OneTag cannot call tools. By default the adapter fails when tools are
|
|
14
|
+
* registered in the model context, so a tool UI does not silently never fire.
|
|
15
|
+
* Set this to send text only and leave the tools out.
|
|
16
|
+
*/
|
|
17
|
+
ignoreTools?: boolean;
|
|
18
|
+
/** Override the endpoint base URL. Defaults to https://onetagai.com/v1. */
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
/** Model name. OneTag accepts any value and always answers as "onetag". */
|
|
21
|
+
model?: string;
|
|
22
|
+
/** Custom fetch, for tests. */
|
|
23
|
+
fetch?: typeof globalThis.fetch;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* A ChatModelAdapter for assistant-ui's LocalRuntime that streams from OneTag's
|
|
27
|
+
* OpenAI-compatible endpoint, straight from the browser. Text only.
|
|
28
|
+
*
|
|
29
|
+
* const runtime = useLocalRuntime(createOneTagAdapter({ apiKey: "ot_..." }));
|
|
30
|
+
*/
|
|
31
|
+
export declare function createOneTagAdapter(options: OneTagAdapterOptions): ChatModelAdapter;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown by the OneTag adapter. `code` is OneTag's error code
|
|
3
|
+
* (bad_key, domain_not_allowed, origin_required, quota_exceeded, rate_limited,
|
|
4
|
+
* too_large, paused, upstream, unsupported) or one of missing_key, network,
|
|
5
|
+
* http_<status>. assistant-ui keeps `code`, `message` and `statusCode` on the
|
|
6
|
+
* failed message's status, so the thread's error UI shows the readable text.
|
|
7
|
+
*/
|
|
8
|
+
export declare class OneTagError extends Error {
|
|
9
|
+
readonly name = "OneTagError";
|
|
10
|
+
readonly code: string;
|
|
11
|
+
readonly statusCode?: number;
|
|
12
|
+
constructor(code: string, message: string, statusCode?: number);
|
|
13
|
+
}
|
|
14
|
+
export declare function readableError(code: string, serverMessage: string | undefined, status?: number): OneTagError;
|
|
15
|
+
export declare function errorFromResponse(res: Response): Promise<OneTagError>;
|
|
16
|
+
export declare const missingKeyError: () => OneTagError;
|
|
17
|
+
export declare const networkError: (cause: unknown) => OneTagError;
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
21
|
+
|
|
22
|
+
// src/index.ts
|
|
23
|
+
var index_exports = {};
|
|
24
|
+
__export(index_exports, {
|
|
25
|
+
ONETAG_BASE_URL: () => ONETAG_BASE_URL,
|
|
26
|
+
OneTagError: () => OneTagError,
|
|
27
|
+
createOneTagAdapter: () => createOneTagAdapter,
|
|
28
|
+
toOneTagMessages: () => toOneTagMessages,
|
|
29
|
+
useOneTagRuntime: () => useOneTagRuntime
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
|
|
33
|
+
// src/errors.ts
|
|
34
|
+
var SIGNUP = "https://onetagai.com/#get-key";
|
|
35
|
+
var OneTagError = class extends Error {
|
|
36
|
+
constructor(code, message, statusCode) {
|
|
37
|
+
super(message);
|
|
38
|
+
__publicField(this, "name", "OneTagError");
|
|
39
|
+
__publicField(this, "code");
|
|
40
|
+
__publicField(this, "statusCode");
|
|
41
|
+
this.code = code;
|
|
42
|
+
if (statusCode !== void 0) this.statusCode = statusCode;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var READABLE = {
|
|
46
|
+
bad_key: `This OneTag key is not valid. Check the key from your signup email, or get one at ${SIGNUP}.`,
|
|
47
|
+
domain_not_allowed: "This OneTag key is not allowed on this domain. Add this site's domain to the key in your OneTag account.",
|
|
48
|
+
origin_required: "OneTag answers only from a browser page or browser extension. Run this adapter in the browser (LocalRuntime), not in server-side code, SSR or a route handler.",
|
|
49
|
+
quota_exceeded: "This OneTag key has used all of its tokens. The free tier is 1,000,000 tokens, one time, no monthly reset; upgrade the key or use another one.",
|
|
50
|
+
rate_limited: "Too many requests to OneTag right now. Wait a moment and try again.",
|
|
51
|
+
too_large: "The conversation is too long for one OneTag request. Start a new thread or send a shorter message.",
|
|
52
|
+
paused: "This OneTag key is paused. Check your OneTag account.",
|
|
53
|
+
upstream: "The model failed to answer. Try again.",
|
|
54
|
+
unsupported: "OneTag is text only: it does not take tools, images, audio or files."
|
|
55
|
+
};
|
|
56
|
+
function readableError(code, serverMessage, status) {
|
|
57
|
+
const base = READABLE[code] ?? `OneTag answered ${status ?? "with an error"}.`;
|
|
58
|
+
const detail = serverMessage && serverMessage.trim() ? ` (OneTag: ${serverMessage.trim()})` : "";
|
|
59
|
+
return new OneTagError(code, base + detail, status);
|
|
60
|
+
}
|
|
61
|
+
async function errorFromResponse(res) {
|
|
62
|
+
let code = `http_${res.status}`;
|
|
63
|
+
let message;
|
|
64
|
+
try {
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
if (data?.error) {
|
|
67
|
+
if (typeof data.error.code === "string") code = data.error.code;
|
|
68
|
+
if (typeof data.error.message === "string") message = data.error.message;
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
return readableError(code, message, res.status);
|
|
73
|
+
}
|
|
74
|
+
var missingKeyError = () => new OneTagError(
|
|
75
|
+
"missing_key",
|
|
76
|
+
`No OneTag key set. Get a free key by email at ${SIGNUP} (no card), then pass it as apiKey.`
|
|
77
|
+
);
|
|
78
|
+
var networkError = (cause) => {
|
|
79
|
+
const e = new OneTagError("network", "Could not reach OneTag. Check your connection and try again.");
|
|
80
|
+
e.cause = cause;
|
|
81
|
+
return e;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// src/messages.ts
|
|
85
|
+
var SKIPPED = /* @__PURE__ */ new Set(["reasoning", "source"]);
|
|
86
|
+
var unsupported = (what) => new OneTagError(
|
|
87
|
+
"unsupported",
|
|
88
|
+
`OneTag is text only, so this message cannot be sent: it contains ${what}. Remove it, or use a runtime with a multimodal or tool-calling model for this thread.`
|
|
89
|
+
);
|
|
90
|
+
function toOneTagMessages(messages, system) {
|
|
91
|
+
const systems = [];
|
|
92
|
+
if (system && system.trim()) systems.push(system);
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const message of messages) {
|
|
95
|
+
const attachments = message.role === "user" ? message.attachments ?? [] : [];
|
|
96
|
+
if (attachments.length > 0) {
|
|
97
|
+
const names = attachments.map((a) => `"${a.name}"`).join(", ");
|
|
98
|
+
throw unsupported(`an attachment (${names})`);
|
|
99
|
+
}
|
|
100
|
+
const texts = [];
|
|
101
|
+
for (const part of message.content) {
|
|
102
|
+
if (part.type === "text") {
|
|
103
|
+
if (part.text) texts.push(part.text);
|
|
104
|
+
} else if (part.type === "tool-call") {
|
|
105
|
+
throw unsupported(`a tool call "${part.toolName ?? "unknown"}"`);
|
|
106
|
+
} else if (!SKIPPED.has(part.type)) {
|
|
107
|
+
throw unsupported(`a ${part.type} part`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const content = texts.join("\n\n");
|
|
111
|
+
if (message.role === "system") {
|
|
112
|
+
if (content) systems.push(content);
|
|
113
|
+
} else if (content) {
|
|
114
|
+
out.push({ role: message.role, content });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return systems.length ? [{ role: "system", content: systems.join("\n\n") }, ...out] : out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/sse.ts
|
|
121
|
+
async function* sseData(body) {
|
|
122
|
+
const reader = body.getReader();
|
|
123
|
+
const decoder = new TextDecoder();
|
|
124
|
+
let buffer = "";
|
|
125
|
+
try {
|
|
126
|
+
for (; ; ) {
|
|
127
|
+
const { done, value } = await reader.read();
|
|
128
|
+
buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
|
|
129
|
+
buffer = buffer.replace(/\r\n?/g, "\n");
|
|
130
|
+
let end;
|
|
131
|
+
while ((end = buffer.indexOf("\n\n")) !== -1) {
|
|
132
|
+
const event = buffer.slice(0, end);
|
|
133
|
+
buffer = buffer.slice(end + 2);
|
|
134
|
+
const data = event.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
|
|
135
|
+
if (data) yield data;
|
|
136
|
+
}
|
|
137
|
+
if (done) {
|
|
138
|
+
const rest = buffer.trim();
|
|
139
|
+
if (rest.startsWith("data:")) yield rest.slice(5).trim();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} finally {
|
|
144
|
+
reader.releaseLock();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/adapter.ts
|
|
149
|
+
var ONETAG_BASE_URL = "https://onetagai.com/v1";
|
|
150
|
+
var isAbort = (e) => typeof e === "object" && e !== null && e.name === "AbortError";
|
|
151
|
+
function createOneTagAdapter(options) {
|
|
152
|
+
return {
|
|
153
|
+
async *run({ messages, abortSignal, context }) {
|
|
154
|
+
if (!options.apiKey) throw missingKeyError();
|
|
155
|
+
const toolNames = Object.keys(context?.tools ?? {});
|
|
156
|
+
if (toolNames.length && !options.ignoreTools) {
|
|
157
|
+
throw new OneTagError(
|
|
158
|
+
"unsupported",
|
|
159
|
+
`OneTag is text only and cannot call tools, but tools are registered (${toolNames.join(", ")}). Remove them, or pass ignoreTools: true to send text without them.`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const system = [options.system, context?.system].filter((s) => s && s.trim()).join("\n\n");
|
|
163
|
+
const body = {
|
|
164
|
+
model: options.model ?? "onetag",
|
|
165
|
+
stream: true,
|
|
166
|
+
messages: toOneTagMessages(messages, system)
|
|
167
|
+
};
|
|
168
|
+
const maxTokens = context?.callSettings?.maxTokens ?? options.maxTokens;
|
|
169
|
+
if (maxTokens !== void 0) body.max_tokens = maxTokens;
|
|
170
|
+
const temperature = context?.callSettings?.temperature ?? options.temperature;
|
|
171
|
+
if (temperature !== void 0) body.temperature = temperature;
|
|
172
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
173
|
+
let res;
|
|
174
|
+
try {
|
|
175
|
+
res = await doFetch(`${(options.baseUrl ?? ONETAG_BASE_URL).replace(/\/+$/, "")}/chat/completions`, {
|
|
176
|
+
method: "POST",
|
|
177
|
+
headers: {
|
|
178
|
+
"Content-Type": "application/json",
|
|
179
|
+
Authorization: `Bearer ${options.apiKey}`
|
|
180
|
+
},
|
|
181
|
+
body: JSON.stringify(body),
|
|
182
|
+
signal: abortSignal
|
|
183
|
+
});
|
|
184
|
+
} catch (e) {
|
|
185
|
+
if (isAbort(e)) throw e;
|
|
186
|
+
throw networkError(e);
|
|
187
|
+
}
|
|
188
|
+
if (!res.ok) throw await errorFromResponse(res);
|
|
189
|
+
if (!res.body) throw new OneTagError("network", "OneTag sent an empty response. Try again.");
|
|
190
|
+
let text = "";
|
|
191
|
+
let finish = null;
|
|
192
|
+
try {
|
|
193
|
+
for await (const data of sseData(res.body)) {
|
|
194
|
+
if (data === "[DONE]") break;
|
|
195
|
+
let parsed;
|
|
196
|
+
try {
|
|
197
|
+
parsed = JSON.parse(data);
|
|
198
|
+
} catch {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (parsed.error) {
|
|
202
|
+
throw readableError(parsed.error.code ?? "upstream", parsed.error.message);
|
|
203
|
+
}
|
|
204
|
+
const choice = parsed.choices?.[0];
|
|
205
|
+
if (!choice) continue;
|
|
206
|
+
const delta = choice.delta?.content;
|
|
207
|
+
if (delta) {
|
|
208
|
+
text += delta;
|
|
209
|
+
yield { content: [{ type: "text", text }] };
|
|
210
|
+
}
|
|
211
|
+
if (choice.finish_reason) finish = choice.finish_reason;
|
|
212
|
+
}
|
|
213
|
+
} catch (e) {
|
|
214
|
+
if (e instanceof OneTagError || isAbort(e)) throw e;
|
|
215
|
+
throw networkError(e);
|
|
216
|
+
}
|
|
217
|
+
const content = text ? [{ type: "text", text }] : [];
|
|
218
|
+
if (finish === "length") {
|
|
219
|
+
yield { content, status: { type: "incomplete", reason: "length" } };
|
|
220
|
+
} else {
|
|
221
|
+
yield { content, status: { type: "complete", reason: finish === "stop" ? "stop" : "unknown" } };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/runtime.ts
|
|
228
|
+
var import_react = require("react");
|
|
229
|
+
var import_react2 = require("@assistant-ui/react");
|
|
230
|
+
function useOneTagRuntime({ runtime, ...options }) {
|
|
231
|
+
const { apiKey, system, maxTokens, temperature, ignoreTools, baseUrl, model, fetch } = options;
|
|
232
|
+
const adapter = (0, import_react.useMemo)(
|
|
233
|
+
() => createOneTagAdapter({ apiKey, system, maxTokens, temperature, ignoreTools, baseUrl, model, fetch }),
|
|
234
|
+
[apiKey, system, maxTokens, temperature, ignoreTools, baseUrl, model, fetch]
|
|
235
|
+
);
|
|
236
|
+
return (0, import_react2.useLocalRuntime)(adapter, runtime);
|
|
237
|
+
}
|
|
238
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
239
|
+
0 && (module.exports = {
|
|
240
|
+
ONETAG_BASE_URL,
|
|
241
|
+
OneTagError,
|
|
242
|
+
createOneTagAdapter,
|
|
243
|
+
toOneTagMessages,
|
|
244
|
+
useOneTagRuntime
|
|
245
|
+
});
|
|
246
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/messages.ts","../src/sse.ts","../src/adapter.ts","../src/runtime.ts"],"sourcesContent":["export { createOneTagAdapter, ONETAG_BASE_URL, type OneTagAdapterOptions } from \"./adapter.js\";\nexport { OneTagError } from \"./errors.js\";\nexport { toOneTagMessages, type OneTagChatMessage } from \"./messages.js\";\nexport { useOneTagRuntime, type UseOneTagRuntimeOptions } from \"./runtime.js\";\n","const SIGNUP = \"https://onetagai.com/#get-key\";\n\n/**\n * Error thrown by the OneTag adapter. `code` is OneTag's error code\n * (bad_key, domain_not_allowed, origin_required, quota_exceeded, rate_limited,\n * too_large, paused, upstream, unsupported) or one of missing_key, network,\n * http_<status>. assistant-ui keeps `code`, `message` and `statusCode` on the\n * failed message's status, so the thread's error UI shows the readable text.\n */\nexport class OneTagError extends Error {\n override readonly name = \"OneTagError\";\n readonly code: string;\n readonly statusCode?: number;\n\n constructor(code: string, message: string, statusCode?: number) {\n super(message);\n this.code = code;\n if (statusCode !== undefined) this.statusCode = statusCode;\n }\n}\n\nconst READABLE: Record<string, string> = {\n bad_key: `This OneTag key is not valid. Check the key from your signup email, or get one at ${SIGNUP}.`,\n domain_not_allowed:\n \"This OneTag key is not allowed on this domain. Add this site's domain to the key in your OneTag account.\",\n origin_required:\n \"OneTag answers only from a browser page or browser extension. Run this adapter in the browser (LocalRuntime), not in server-side code, SSR or a route handler.\",\n quota_exceeded:\n \"This OneTag key has used all of its tokens. The free tier is 1,000,000 tokens, one time, no monthly reset; upgrade the key or use another one.\",\n rate_limited: \"Too many requests to OneTag right now. Wait a moment and try again.\",\n too_large: \"The conversation is too long for one OneTag request. Start a new thread or send a shorter message.\",\n paused: \"This OneTag key is paused. Check your OneTag account.\",\n upstream: \"The model failed to answer. Try again.\",\n unsupported: \"OneTag is text only: it does not take tools, images, audio or files.\",\n};\n\nexport function readableError(code: string, serverMessage: string | undefined, status?: number): OneTagError {\n const base = READABLE[code] ?? `OneTag answered ${status ?? \"with an error\"}.`;\n const detail = serverMessage && serverMessage.trim() ? ` (OneTag: ${serverMessage.trim()})` : \"\";\n return new OneTagError(code, base + detail, status);\n}\n\nexport async function errorFromResponse(res: Response): Promise<OneTagError> {\n let code = `http_${res.status}`;\n let message: string | undefined;\n try {\n const data = (await res.json()) as { error?: { code?: unknown; message?: unknown } };\n if (data?.error) {\n if (typeof data.error.code === \"string\") code = data.error.code;\n if (typeof data.error.message === \"string\") message = data.error.message;\n }\n } catch {\n // not JSON; keep the status code\n }\n return readableError(code, message, res.status);\n}\n\nexport const missingKeyError = () =>\n new OneTagError(\n \"missing_key\",\n `No OneTag key set. Get a free key by email at ${SIGNUP} (no card), then pass it as apiKey.`,\n );\n\nexport const networkError = (cause: unknown) => {\n const e = new OneTagError(\"network\", \"Could not reach OneTag. Check your connection and try again.\");\n (e as { cause?: unknown }).cause = cause;\n return e;\n};\n","import type { ThreadMessage } from \"@assistant-ui/react\";\nimport { OneTagError } from \"./errors.js\";\n\nexport type OneTagChatMessage = {\n role: \"system\" | \"user\" | \"assistant\";\n content: string;\n};\n\n/** Parts that are display metadata, not model input. They are left out of the request. */\nconst SKIPPED = new Set([\"reasoning\", \"source\"]);\n\nconst unsupported = (what: string) =>\n new OneTagError(\n \"unsupported\",\n `OneTag is text only, so this message cannot be sent: it contains ${what}. Remove it, or use a runtime with a multimodal or tool-calling model for this thread.`,\n );\n\n/**\n * Convert assistant-ui thread messages to OpenAI chat messages with plain text\n * content. Throws a OneTagError (code \"unsupported\") on attachments, images,\n * files, audio, data parts and tool calls instead of dropping them silently.\n */\nexport function toOneTagMessages(\n messages: readonly ThreadMessage[],\n system?: string,\n): OneTagChatMessage[] {\n const systems: string[] = [];\n if (system && system.trim()) systems.push(system);\n const out: OneTagChatMessage[] = [];\n\n for (const message of messages) {\n const attachments = message.role === \"user\" ? message.attachments ?? [] : [];\n if (attachments.length > 0) {\n const names = attachments.map((a) => `\"${a.name}\"`).join(\", \");\n throw unsupported(`an attachment (${names})`);\n }\n\n const texts: string[] = [];\n for (const part of message.content as readonly { type: string; text?: string; toolName?: string }[]) {\n if (part.type === \"text\") {\n if (part.text) texts.push(part.text);\n } else if (part.type === \"tool-call\") {\n throw unsupported(`a tool call \"${part.toolName ?? \"unknown\"}\"`);\n } else if (!SKIPPED.has(part.type)) {\n throw unsupported(`a ${part.type} part`);\n }\n }\n const content = texts.join(\"\\n\\n\");\n\n if (message.role === \"system\") {\n if (content) systems.push(content);\n } else if (content) {\n out.push({ role: message.role, content });\n }\n }\n\n return systems.length ? [{ role: \"system\", content: systems.join(\"\\n\\n\") }, ...out] : out;\n}\n","/** Yield the data payload of each server-sent event in a fetch body. */\nexport async function* sseData(body: ReadableStream<Uint8Array>): AsyncGenerator<string> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n try {\n for (;;) {\n const { done, value } = await reader.read();\n buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });\n buffer = buffer.replace(/\\r\\n?/g, \"\\n\");\n let end: number;\n while ((end = buffer.indexOf(\"\\n\\n\")) !== -1) {\n const event = buffer.slice(0, end);\n buffer = buffer.slice(end + 2);\n const data = event\n .split(\"\\n\")\n .filter((line) => line.startsWith(\"data:\"))\n .map((line) => line.slice(5).replace(/^ /, \"\"))\n .join(\"\\n\");\n if (data) yield data;\n }\n if (done) {\n const rest = buffer.trim();\n if (rest.startsWith(\"data:\")) yield rest.slice(5).trim();\n return;\n }\n }\n } finally {\n reader.releaseLock();\n }\n}\n","import type { ChatModelAdapter, ChatModelRunResult } from \"@assistant-ui/react\";\nimport { errorFromResponse, missingKeyError, networkError, OneTagError, readableError } from \"./errors.js\";\nimport { toOneTagMessages } from \"./messages.js\";\nimport { sseData } from \"./sse.js\";\n\nexport const ONETAG_BASE_URL = \"https://onetagai.com/v1\";\n\nexport type OneTagAdapterOptions = {\n /** Your OneTag key. It is public by design: it can sit in the page's HTML or JS bundle. */\n apiKey: string;\n /** System prompt, sent before any system prompt from the model context (useAssistantInstructions). */\n system?: string;\n /** Reply length cap. OneTag defaults to 2000 and caps at 10000. A model context callSettings.maxTokens wins. */\n maxTokens?: number;\n /** Sampling temperature. A model context callSettings.temperature wins. */\n temperature?: number;\n /**\n * OneTag cannot call tools. By default the adapter fails when tools are\n * registered in the model context, so a tool UI does not silently never fire.\n * Set this to send text only and leave the tools out.\n */\n ignoreTools?: boolean;\n /** Override the endpoint base URL. Defaults to https://onetagai.com/v1. */\n baseUrl?: string;\n /** Model name. OneTag accepts any value and always answers as \"onetag\". */\n model?: string;\n /** Custom fetch, for tests. */\n fetch?: typeof globalThis.fetch;\n};\n\ntype Chunk = {\n error?: { code?: string; message?: string };\n choices?: { delta?: { content?: string | null }; finish_reason?: string | null }[];\n};\n\nconst isAbort = (e: unknown) =>\n typeof e === \"object\" && e !== null && (e as { name?: string }).name === \"AbortError\";\n\n/**\n * A ChatModelAdapter for assistant-ui's LocalRuntime that streams from OneTag's\n * OpenAI-compatible endpoint, straight from the browser. Text only.\n *\n * const runtime = useLocalRuntime(createOneTagAdapter({ apiKey: \"ot_...\" }));\n */\nexport function createOneTagAdapter(options: OneTagAdapterOptions): ChatModelAdapter {\n return {\n async *run({ messages, abortSignal, context }) {\n if (!options.apiKey) throw missingKeyError();\n\n const toolNames = Object.keys(context?.tools ?? {});\n if (toolNames.length && !options.ignoreTools) {\n throw new OneTagError(\n \"unsupported\",\n `OneTag is text only and cannot call tools, but tools are registered (${toolNames.join(\", \")}). Remove them, or pass ignoreTools: true to send text without them.`,\n );\n }\n\n const system = [options.system, context?.system].filter((s) => s && s.trim()).join(\"\\n\\n\");\n const body: Record<string, unknown> = {\n model: options.model ?? \"onetag\",\n stream: true,\n messages: toOneTagMessages(messages, system),\n };\n const maxTokens = context?.callSettings?.maxTokens ?? options.maxTokens;\n if (maxTokens !== undefined) body.max_tokens = maxTokens;\n const temperature = context?.callSettings?.temperature ?? options.temperature;\n if (temperature !== undefined) body.temperature = temperature;\n\n const doFetch = options.fetch ?? globalThis.fetch;\n let res: Response;\n try {\n res = await doFetch(`${(options.baseUrl ?? ONETAG_BASE_URL).replace(/\\/+$/, \"\")}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${options.apiKey}`,\n },\n body: JSON.stringify(body),\n signal: abortSignal,\n });\n } catch (e) {\n if (isAbort(e)) throw e;\n throw networkError(e);\n }\n if (!res.ok) throw await errorFromResponse(res);\n if (!res.body) throw new OneTagError(\"network\", \"OneTag sent an empty response. Try again.\");\n\n let text = \"\";\n let finish: string | null = null;\n try {\n for await (const data of sseData(res.body)) {\n if (data === \"[DONE]\") break;\n let parsed: Chunk;\n try {\n parsed = JSON.parse(data) as Chunk;\n } catch {\n continue;\n }\n if (parsed.error) {\n throw readableError(parsed.error.code ?? \"upstream\", parsed.error.message);\n }\n const choice = parsed.choices?.[0];\n if (!choice) continue;\n const delta = choice.delta?.content;\n if (delta) {\n text += delta;\n yield { content: [{ type: \"text\", text }] };\n }\n if (choice.finish_reason) finish = choice.finish_reason;\n }\n } catch (e) {\n if (e instanceof OneTagError || isAbort(e)) throw e;\n throw networkError(e);\n }\n\n const content: ChatModelRunResult[\"content\"] = text ? [{ type: \"text\", text }] : [];\n if (finish === \"length\") {\n yield { content, status: { type: \"incomplete\", reason: \"length\" } };\n } else {\n yield { content, status: { type: \"complete\", reason: finish === \"stop\" ? \"stop\" : \"unknown\" } };\n }\n },\n };\n}\n","import { useMemo } from \"react\";\nimport { useLocalRuntime, type AssistantRuntime } from \"@assistant-ui/react\";\nimport { createOneTagAdapter, type OneTagAdapterOptions } from \"./adapter.js\";\n\nexport type UseOneTagRuntimeOptions = OneTagAdapterOptions & {\n /** Passed through to useLocalRuntime (adapters, initialMessages, cloud, ...). */\n runtime?: Parameters<typeof useLocalRuntime>[1];\n};\n\n/**\n * LocalRuntime answered by OneTag, from the browser. No API route.\n *\n * const runtime = useOneTagRuntime({ apiKey: \"ot_...\" });\n * <AssistantRuntimeProvider runtime={runtime}><Thread /></AssistantRuntimeProvider>\n */\nexport function useOneTagRuntime({ runtime, ...options }: UseOneTagRuntimeOptions): AssistantRuntime {\n const { apiKey, system, maxTokens, temperature, ignoreTools, baseUrl, model, fetch } = options;\n const adapter = useMemo(\n () => createOneTagAdapter({ apiKey, system, maxTokens, temperature, ignoreTools, baseUrl, model, fetch }),\n [apiKey, system, maxTokens, temperature, ignoreTools, baseUrl, model, fetch],\n );\n return useLocalRuntime(adapter, runtime);\n}\n"],"mappings":"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","names":["import_react"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createOneTagAdapter, ONETAG_BASE_URL, type OneTagAdapterOptions } from "./adapter.js";
|
|
2
|
+
export { OneTagError } from "./errors.js";
|
|
3
|
+
export { toOneTagMessages, type OneTagChatMessage } from "./messages.js";
|
|
4
|
+
export { useOneTagRuntime, type UseOneTagRuntimeOptions } from "./runtime.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/errors.ts
|
|
6
|
+
var SIGNUP = "https://onetagai.com/#get-key";
|
|
7
|
+
var OneTagError = class extends Error {
|
|
8
|
+
constructor(code, message, statusCode) {
|
|
9
|
+
super(message);
|
|
10
|
+
__publicField(this, "name", "OneTagError");
|
|
11
|
+
__publicField(this, "code");
|
|
12
|
+
__publicField(this, "statusCode");
|
|
13
|
+
this.code = code;
|
|
14
|
+
if (statusCode !== void 0) this.statusCode = statusCode;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var READABLE = {
|
|
18
|
+
bad_key: `This OneTag key is not valid. Check the key from your signup email, or get one at ${SIGNUP}.`,
|
|
19
|
+
domain_not_allowed: "This OneTag key is not allowed on this domain. Add this site's domain to the key in your OneTag account.",
|
|
20
|
+
origin_required: "OneTag answers only from a browser page or browser extension. Run this adapter in the browser (LocalRuntime), not in server-side code, SSR or a route handler.",
|
|
21
|
+
quota_exceeded: "This OneTag key has used all of its tokens. The free tier is 1,000,000 tokens, one time, no monthly reset; upgrade the key or use another one.",
|
|
22
|
+
rate_limited: "Too many requests to OneTag right now. Wait a moment and try again.",
|
|
23
|
+
too_large: "The conversation is too long for one OneTag request. Start a new thread or send a shorter message.",
|
|
24
|
+
paused: "This OneTag key is paused. Check your OneTag account.",
|
|
25
|
+
upstream: "The model failed to answer. Try again.",
|
|
26
|
+
unsupported: "OneTag is text only: it does not take tools, images, audio or files."
|
|
27
|
+
};
|
|
28
|
+
function readableError(code, serverMessage, status) {
|
|
29
|
+
const base = READABLE[code] ?? `OneTag answered ${status ?? "with an error"}.`;
|
|
30
|
+
const detail = serverMessage && serverMessage.trim() ? ` (OneTag: ${serverMessage.trim()})` : "";
|
|
31
|
+
return new OneTagError(code, base + detail, status);
|
|
32
|
+
}
|
|
33
|
+
async function errorFromResponse(res) {
|
|
34
|
+
let code = `http_${res.status}`;
|
|
35
|
+
let message;
|
|
36
|
+
try {
|
|
37
|
+
const data = await res.json();
|
|
38
|
+
if (data?.error) {
|
|
39
|
+
if (typeof data.error.code === "string") code = data.error.code;
|
|
40
|
+
if (typeof data.error.message === "string") message = data.error.message;
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
return readableError(code, message, res.status);
|
|
45
|
+
}
|
|
46
|
+
var missingKeyError = () => new OneTagError(
|
|
47
|
+
"missing_key",
|
|
48
|
+
`No OneTag key set. Get a free key by email at ${SIGNUP} (no card), then pass it as apiKey.`
|
|
49
|
+
);
|
|
50
|
+
var networkError = (cause) => {
|
|
51
|
+
const e = new OneTagError("network", "Could not reach OneTag. Check your connection and try again.");
|
|
52
|
+
e.cause = cause;
|
|
53
|
+
return e;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// src/messages.ts
|
|
57
|
+
var SKIPPED = /* @__PURE__ */ new Set(["reasoning", "source"]);
|
|
58
|
+
var unsupported = (what) => new OneTagError(
|
|
59
|
+
"unsupported",
|
|
60
|
+
`OneTag is text only, so this message cannot be sent: it contains ${what}. Remove it, or use a runtime with a multimodal or tool-calling model for this thread.`
|
|
61
|
+
);
|
|
62
|
+
function toOneTagMessages(messages, system) {
|
|
63
|
+
const systems = [];
|
|
64
|
+
if (system && system.trim()) systems.push(system);
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const message of messages) {
|
|
67
|
+
const attachments = message.role === "user" ? message.attachments ?? [] : [];
|
|
68
|
+
if (attachments.length > 0) {
|
|
69
|
+
const names = attachments.map((a) => `"${a.name}"`).join(", ");
|
|
70
|
+
throw unsupported(`an attachment (${names})`);
|
|
71
|
+
}
|
|
72
|
+
const texts = [];
|
|
73
|
+
for (const part of message.content) {
|
|
74
|
+
if (part.type === "text") {
|
|
75
|
+
if (part.text) texts.push(part.text);
|
|
76
|
+
} else if (part.type === "tool-call") {
|
|
77
|
+
throw unsupported(`a tool call "${part.toolName ?? "unknown"}"`);
|
|
78
|
+
} else if (!SKIPPED.has(part.type)) {
|
|
79
|
+
throw unsupported(`a ${part.type} part`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const content = texts.join("\n\n");
|
|
83
|
+
if (message.role === "system") {
|
|
84
|
+
if (content) systems.push(content);
|
|
85
|
+
} else if (content) {
|
|
86
|
+
out.push({ role: message.role, content });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return systems.length ? [{ role: "system", content: systems.join("\n\n") }, ...out] : out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/sse.ts
|
|
93
|
+
async function* sseData(body) {
|
|
94
|
+
const reader = body.getReader();
|
|
95
|
+
const decoder = new TextDecoder();
|
|
96
|
+
let buffer = "";
|
|
97
|
+
try {
|
|
98
|
+
for (; ; ) {
|
|
99
|
+
const { done, value } = await reader.read();
|
|
100
|
+
buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
|
|
101
|
+
buffer = buffer.replace(/\r\n?/g, "\n");
|
|
102
|
+
let end;
|
|
103
|
+
while ((end = buffer.indexOf("\n\n")) !== -1) {
|
|
104
|
+
const event = buffer.slice(0, end);
|
|
105
|
+
buffer = buffer.slice(end + 2);
|
|
106
|
+
const data = event.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
|
|
107
|
+
if (data) yield data;
|
|
108
|
+
}
|
|
109
|
+
if (done) {
|
|
110
|
+
const rest = buffer.trim();
|
|
111
|
+
if (rest.startsWith("data:")) yield rest.slice(5).trim();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
reader.releaseLock();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/adapter.ts
|
|
121
|
+
var ONETAG_BASE_URL = "https://onetagai.com/v1";
|
|
122
|
+
var isAbort = (e) => typeof e === "object" && e !== null && e.name === "AbortError";
|
|
123
|
+
function createOneTagAdapter(options) {
|
|
124
|
+
return {
|
|
125
|
+
async *run({ messages, abortSignal, context }) {
|
|
126
|
+
if (!options.apiKey) throw missingKeyError();
|
|
127
|
+
const toolNames = Object.keys(context?.tools ?? {});
|
|
128
|
+
if (toolNames.length && !options.ignoreTools) {
|
|
129
|
+
throw new OneTagError(
|
|
130
|
+
"unsupported",
|
|
131
|
+
`OneTag is text only and cannot call tools, but tools are registered (${toolNames.join(", ")}). Remove them, or pass ignoreTools: true to send text without them.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const system = [options.system, context?.system].filter((s) => s && s.trim()).join("\n\n");
|
|
135
|
+
const body = {
|
|
136
|
+
model: options.model ?? "onetag",
|
|
137
|
+
stream: true,
|
|
138
|
+
messages: toOneTagMessages(messages, system)
|
|
139
|
+
};
|
|
140
|
+
const maxTokens = context?.callSettings?.maxTokens ?? options.maxTokens;
|
|
141
|
+
if (maxTokens !== void 0) body.max_tokens = maxTokens;
|
|
142
|
+
const temperature = context?.callSettings?.temperature ?? options.temperature;
|
|
143
|
+
if (temperature !== void 0) body.temperature = temperature;
|
|
144
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
145
|
+
let res;
|
|
146
|
+
try {
|
|
147
|
+
res = await doFetch(`${(options.baseUrl ?? ONETAG_BASE_URL).replace(/\/+$/, "")}/chat/completions`, {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: {
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
Authorization: `Bearer ${options.apiKey}`
|
|
152
|
+
},
|
|
153
|
+
body: JSON.stringify(body),
|
|
154
|
+
signal: abortSignal
|
|
155
|
+
});
|
|
156
|
+
} catch (e) {
|
|
157
|
+
if (isAbort(e)) throw e;
|
|
158
|
+
throw networkError(e);
|
|
159
|
+
}
|
|
160
|
+
if (!res.ok) throw await errorFromResponse(res);
|
|
161
|
+
if (!res.body) throw new OneTagError("network", "OneTag sent an empty response. Try again.");
|
|
162
|
+
let text = "";
|
|
163
|
+
let finish = null;
|
|
164
|
+
try {
|
|
165
|
+
for await (const data of sseData(res.body)) {
|
|
166
|
+
if (data === "[DONE]") break;
|
|
167
|
+
let parsed;
|
|
168
|
+
try {
|
|
169
|
+
parsed = JSON.parse(data);
|
|
170
|
+
} catch {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (parsed.error) {
|
|
174
|
+
throw readableError(parsed.error.code ?? "upstream", parsed.error.message);
|
|
175
|
+
}
|
|
176
|
+
const choice = parsed.choices?.[0];
|
|
177
|
+
if (!choice) continue;
|
|
178
|
+
const delta = choice.delta?.content;
|
|
179
|
+
if (delta) {
|
|
180
|
+
text += delta;
|
|
181
|
+
yield { content: [{ type: "text", text }] };
|
|
182
|
+
}
|
|
183
|
+
if (choice.finish_reason) finish = choice.finish_reason;
|
|
184
|
+
}
|
|
185
|
+
} catch (e) {
|
|
186
|
+
if (e instanceof OneTagError || isAbort(e)) throw e;
|
|
187
|
+
throw networkError(e);
|
|
188
|
+
}
|
|
189
|
+
const content = text ? [{ type: "text", text }] : [];
|
|
190
|
+
if (finish === "length") {
|
|
191
|
+
yield { content, status: { type: "incomplete", reason: "length" } };
|
|
192
|
+
} else {
|
|
193
|
+
yield { content, status: { type: "complete", reason: finish === "stop" ? "stop" : "unknown" } };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/runtime.ts
|
|
200
|
+
import { useMemo } from "react";
|
|
201
|
+
import { useLocalRuntime } from "@assistant-ui/react";
|
|
202
|
+
function useOneTagRuntime({ runtime, ...options }) {
|
|
203
|
+
const { apiKey, system, maxTokens, temperature, ignoreTools, baseUrl, model, fetch } = options;
|
|
204
|
+
const adapter = useMemo(
|
|
205
|
+
() => createOneTagAdapter({ apiKey, system, maxTokens, temperature, ignoreTools, baseUrl, model, fetch }),
|
|
206
|
+
[apiKey, system, maxTokens, temperature, ignoreTools, baseUrl, model, fetch]
|
|
207
|
+
);
|
|
208
|
+
return useLocalRuntime(adapter, runtime);
|
|
209
|
+
}
|
|
210
|
+
export {
|
|
211
|
+
ONETAG_BASE_URL,
|
|
212
|
+
OneTagError,
|
|
213
|
+
createOneTagAdapter,
|
|
214
|
+
toOneTagMessages,
|
|
215
|
+
useOneTagRuntime
|
|
216
|
+
};
|
|
217
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/messages.ts","../src/sse.ts","../src/adapter.ts","../src/runtime.ts"],"sourcesContent":["const SIGNUP = \"https://onetagai.com/#get-key\";\n\n/**\n * Error thrown by the OneTag adapter. `code` is OneTag's error code\n * (bad_key, domain_not_allowed, origin_required, quota_exceeded, rate_limited,\n * too_large, paused, upstream, unsupported) or one of missing_key, network,\n * http_<status>. assistant-ui keeps `code`, `message` and `statusCode` on the\n * failed message's status, so the thread's error UI shows the readable text.\n */\nexport class OneTagError extends Error {\n override readonly name = \"OneTagError\";\n readonly code: string;\n readonly statusCode?: number;\n\n constructor(code: string, message: string, statusCode?: number) {\n super(message);\n this.code = code;\n if (statusCode !== undefined) this.statusCode = statusCode;\n }\n}\n\nconst READABLE: Record<string, string> = {\n bad_key: `This OneTag key is not valid. Check the key from your signup email, or get one at ${SIGNUP}.`,\n domain_not_allowed:\n \"This OneTag key is not allowed on this domain. Add this site's domain to the key in your OneTag account.\",\n origin_required:\n \"OneTag answers only from a browser page or browser extension. Run this adapter in the browser (LocalRuntime), not in server-side code, SSR or a route handler.\",\n quota_exceeded:\n \"This OneTag key has used all of its tokens. The free tier is 1,000,000 tokens, one time, no monthly reset; upgrade the key or use another one.\",\n rate_limited: \"Too many requests to OneTag right now. Wait a moment and try again.\",\n too_large: \"The conversation is too long for one OneTag request. Start a new thread or send a shorter message.\",\n paused: \"This OneTag key is paused. Check your OneTag account.\",\n upstream: \"The model failed to answer. Try again.\",\n unsupported: \"OneTag is text only: it does not take tools, images, audio or files.\",\n};\n\nexport function readableError(code: string, serverMessage: string | undefined, status?: number): OneTagError {\n const base = READABLE[code] ?? `OneTag answered ${status ?? \"with an error\"}.`;\n const detail = serverMessage && serverMessage.trim() ? ` (OneTag: ${serverMessage.trim()})` : \"\";\n return new OneTagError(code, base + detail, status);\n}\n\nexport async function errorFromResponse(res: Response): Promise<OneTagError> {\n let code = `http_${res.status}`;\n let message: string | undefined;\n try {\n const data = (await res.json()) as { error?: { code?: unknown; message?: unknown } };\n if (data?.error) {\n if (typeof data.error.code === \"string\") code = data.error.code;\n if (typeof data.error.message === \"string\") message = data.error.message;\n }\n } catch {\n // not JSON; keep the status code\n }\n return readableError(code, message, res.status);\n}\n\nexport const missingKeyError = () =>\n new OneTagError(\n \"missing_key\",\n `No OneTag key set. Get a free key by email at ${SIGNUP} (no card), then pass it as apiKey.`,\n );\n\nexport const networkError = (cause: unknown) => {\n const e = new OneTagError(\"network\", \"Could not reach OneTag. Check your connection and try again.\");\n (e as { cause?: unknown }).cause = cause;\n return e;\n};\n","import type { ThreadMessage } from \"@assistant-ui/react\";\nimport { OneTagError } from \"./errors.js\";\n\nexport type OneTagChatMessage = {\n role: \"system\" | \"user\" | \"assistant\";\n content: string;\n};\n\n/** Parts that are display metadata, not model input. They are left out of the request. */\nconst SKIPPED = new Set([\"reasoning\", \"source\"]);\n\nconst unsupported = (what: string) =>\n new OneTagError(\n \"unsupported\",\n `OneTag is text only, so this message cannot be sent: it contains ${what}. Remove it, or use a runtime with a multimodal or tool-calling model for this thread.`,\n );\n\n/**\n * Convert assistant-ui thread messages to OpenAI chat messages with plain text\n * content. Throws a OneTagError (code \"unsupported\") on attachments, images,\n * files, audio, data parts and tool calls instead of dropping them silently.\n */\nexport function toOneTagMessages(\n messages: readonly ThreadMessage[],\n system?: string,\n): OneTagChatMessage[] {\n const systems: string[] = [];\n if (system && system.trim()) systems.push(system);\n const out: OneTagChatMessage[] = [];\n\n for (const message of messages) {\n const attachments = message.role === \"user\" ? message.attachments ?? [] : [];\n if (attachments.length > 0) {\n const names = attachments.map((a) => `\"${a.name}\"`).join(\", \");\n throw unsupported(`an attachment (${names})`);\n }\n\n const texts: string[] = [];\n for (const part of message.content as readonly { type: string; text?: string; toolName?: string }[]) {\n if (part.type === \"text\") {\n if (part.text) texts.push(part.text);\n } else if (part.type === \"tool-call\") {\n throw unsupported(`a tool call \"${part.toolName ?? \"unknown\"}\"`);\n } else if (!SKIPPED.has(part.type)) {\n throw unsupported(`a ${part.type} part`);\n }\n }\n const content = texts.join(\"\\n\\n\");\n\n if (message.role === \"system\") {\n if (content) systems.push(content);\n } else if (content) {\n out.push({ role: message.role, content });\n }\n }\n\n return systems.length ? [{ role: \"system\", content: systems.join(\"\\n\\n\") }, ...out] : out;\n}\n","/** Yield the data payload of each server-sent event in a fetch body. */\nexport async function* sseData(body: ReadableStream<Uint8Array>): AsyncGenerator<string> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n try {\n for (;;) {\n const { done, value } = await reader.read();\n buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });\n buffer = buffer.replace(/\\r\\n?/g, \"\\n\");\n let end: number;\n while ((end = buffer.indexOf(\"\\n\\n\")) !== -1) {\n const event = buffer.slice(0, end);\n buffer = buffer.slice(end + 2);\n const data = event\n .split(\"\\n\")\n .filter((line) => line.startsWith(\"data:\"))\n .map((line) => line.slice(5).replace(/^ /, \"\"))\n .join(\"\\n\");\n if (data) yield data;\n }\n if (done) {\n const rest = buffer.trim();\n if (rest.startsWith(\"data:\")) yield rest.slice(5).trim();\n return;\n }\n }\n } finally {\n reader.releaseLock();\n }\n}\n","import type { ChatModelAdapter, ChatModelRunResult } from \"@assistant-ui/react\";\nimport { errorFromResponse, missingKeyError, networkError, OneTagError, readableError } from \"./errors.js\";\nimport { toOneTagMessages } from \"./messages.js\";\nimport { sseData } from \"./sse.js\";\n\nexport const ONETAG_BASE_URL = \"https://onetagai.com/v1\";\n\nexport type OneTagAdapterOptions = {\n /** Your OneTag key. It is public by design: it can sit in the page's HTML or JS bundle. */\n apiKey: string;\n /** System prompt, sent before any system prompt from the model context (useAssistantInstructions). */\n system?: string;\n /** Reply length cap. OneTag defaults to 2000 and caps at 10000. A model context callSettings.maxTokens wins. */\n maxTokens?: number;\n /** Sampling temperature. A model context callSettings.temperature wins. */\n temperature?: number;\n /**\n * OneTag cannot call tools. By default the adapter fails when tools are\n * registered in the model context, so a tool UI does not silently never fire.\n * Set this to send text only and leave the tools out.\n */\n ignoreTools?: boolean;\n /** Override the endpoint base URL. Defaults to https://onetagai.com/v1. */\n baseUrl?: string;\n /** Model name. OneTag accepts any value and always answers as \"onetag\". */\n model?: string;\n /** Custom fetch, for tests. */\n fetch?: typeof globalThis.fetch;\n};\n\ntype Chunk = {\n error?: { code?: string; message?: string };\n choices?: { delta?: { content?: string | null }; finish_reason?: string | null }[];\n};\n\nconst isAbort = (e: unknown) =>\n typeof e === \"object\" && e !== null && (e as { name?: string }).name === \"AbortError\";\n\n/**\n * A ChatModelAdapter for assistant-ui's LocalRuntime that streams from OneTag's\n * OpenAI-compatible endpoint, straight from the browser. Text only.\n *\n * const runtime = useLocalRuntime(createOneTagAdapter({ apiKey: \"ot_...\" }));\n */\nexport function createOneTagAdapter(options: OneTagAdapterOptions): ChatModelAdapter {\n return {\n async *run({ messages, abortSignal, context }) {\n if (!options.apiKey) throw missingKeyError();\n\n const toolNames = Object.keys(context?.tools ?? {});\n if (toolNames.length && !options.ignoreTools) {\n throw new OneTagError(\n \"unsupported\",\n `OneTag is text only and cannot call tools, but tools are registered (${toolNames.join(\", \")}). Remove them, or pass ignoreTools: true to send text without them.`,\n );\n }\n\n const system = [options.system, context?.system].filter((s) => s && s.trim()).join(\"\\n\\n\");\n const body: Record<string, unknown> = {\n model: options.model ?? \"onetag\",\n stream: true,\n messages: toOneTagMessages(messages, system),\n };\n const maxTokens = context?.callSettings?.maxTokens ?? options.maxTokens;\n if (maxTokens !== undefined) body.max_tokens = maxTokens;\n const temperature = context?.callSettings?.temperature ?? options.temperature;\n if (temperature !== undefined) body.temperature = temperature;\n\n const doFetch = options.fetch ?? globalThis.fetch;\n let res: Response;\n try {\n res = await doFetch(`${(options.baseUrl ?? ONETAG_BASE_URL).replace(/\\/+$/, \"\")}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${options.apiKey}`,\n },\n body: JSON.stringify(body),\n signal: abortSignal,\n });\n } catch (e) {\n if (isAbort(e)) throw e;\n throw networkError(e);\n }\n if (!res.ok) throw await errorFromResponse(res);\n if (!res.body) throw new OneTagError(\"network\", \"OneTag sent an empty response. Try again.\");\n\n let text = \"\";\n let finish: string | null = null;\n try {\n for await (const data of sseData(res.body)) {\n if (data === \"[DONE]\") break;\n let parsed: Chunk;\n try {\n parsed = JSON.parse(data) as Chunk;\n } catch {\n continue;\n }\n if (parsed.error) {\n throw readableError(parsed.error.code ?? \"upstream\", parsed.error.message);\n }\n const choice = parsed.choices?.[0];\n if (!choice) continue;\n const delta = choice.delta?.content;\n if (delta) {\n text += delta;\n yield { content: [{ type: \"text\", text }] };\n }\n if (choice.finish_reason) finish = choice.finish_reason;\n }\n } catch (e) {\n if (e instanceof OneTagError || isAbort(e)) throw e;\n throw networkError(e);\n }\n\n const content: ChatModelRunResult[\"content\"] = text ? [{ type: \"text\", text }] : [];\n if (finish === \"length\") {\n yield { content, status: { type: \"incomplete\", reason: \"length\" } };\n } else {\n yield { content, status: { type: \"complete\", reason: finish === \"stop\" ? \"stop\" : \"unknown\" } };\n }\n },\n };\n}\n","import { useMemo } from \"react\";\nimport { useLocalRuntime, type AssistantRuntime } from \"@assistant-ui/react\";\nimport { createOneTagAdapter, type OneTagAdapterOptions } from \"./adapter.js\";\n\nexport type UseOneTagRuntimeOptions = OneTagAdapterOptions & {\n /** Passed through to useLocalRuntime (adapters, initialMessages, cloud, ...). */\n runtime?: Parameters<typeof useLocalRuntime>[1];\n};\n\n/**\n * LocalRuntime answered by OneTag, from the browser. No API route.\n *\n * const runtime = useOneTagRuntime({ apiKey: \"ot_...\" });\n * <AssistantRuntimeProvider runtime={runtime}><Thread /></AssistantRuntimeProvider>\n */\nexport function useOneTagRuntime({ runtime, ...options }: UseOneTagRuntimeOptions): AssistantRuntime {\n const { apiKey, system, maxTokens, temperature, ignoreTools, baseUrl, model, fetch } = options;\n const adapter = useMemo(\n () => createOneTagAdapter({ apiKey, system, maxTokens, temperature, ignoreTools, baseUrl, model, fetch }),\n [apiKey, system, maxTokens, temperature, ignoreTools, baseUrl, model, fetch],\n );\n return useLocalRuntime(adapter, runtime);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ThreadMessage } from "@assistant-ui/react";
|
|
2
|
+
export type OneTagChatMessage = {
|
|
3
|
+
role: "system" | "user" | "assistant";
|
|
4
|
+
content: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Convert assistant-ui thread messages to OpenAI chat messages with plain text
|
|
8
|
+
* content. Throws a OneTagError (code "unsupported") on attachments, images,
|
|
9
|
+
* files, audio, data parts and tool calls instead of dropping them silently.
|
|
10
|
+
*/
|
|
11
|
+
export declare function toOneTagMessages(messages: readonly ThreadMessage[], system?: string): OneTagChatMessage[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useLocalRuntime, type AssistantRuntime } from "@assistant-ui/react";
|
|
2
|
+
import { type OneTagAdapterOptions } from "./adapter.js";
|
|
3
|
+
export type UseOneTagRuntimeOptions = OneTagAdapterOptions & {
|
|
4
|
+
/** Passed through to useLocalRuntime (adapters, initialMessages, cloud, ...). */
|
|
5
|
+
runtime?: Parameters<typeof useLocalRuntime>[1];
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* LocalRuntime answered by OneTag, from the browser. No API route.
|
|
9
|
+
*
|
|
10
|
+
* const runtime = useOneTagRuntime({ apiKey: "ot_..." });
|
|
11
|
+
* <AssistantRuntimeProvider runtime={runtime}><Thread /></AssistantRuntimeProvider>
|
|
12
|
+
*/
|
|
13
|
+
export declare function useOneTagRuntime({ runtime, ...options }: UseOneTagRuntimeOptions): AssistantRuntime;
|
package/dist/sse.d.ts
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,74 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assistant-ui-onetag",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OneTag AI chat model adapter for assistant-ui's LocalRuntime: a public, capped key for static pages and browser code, no API route or proxy.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
"./package.json": "./package.json",
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"require": "./dist/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup && tsc -p tsconfig.build.json",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:browser": "node test/browser/run.mjs",
|
|
29
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"assistant-ui",
|
|
33
|
+
"runtime",
|
|
34
|
+
"chat-model-adapter",
|
|
35
|
+
"local-runtime",
|
|
36
|
+
"onetag",
|
|
37
|
+
"llm",
|
|
38
|
+
"chat",
|
|
39
|
+
"browser",
|
|
40
|
+
"static-site",
|
|
41
|
+
"public-api-key"
|
|
42
|
+
],
|
|
43
|
+
"homepage": "https://onetagai.com/docs#openai",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/fengyiqicoder/assistant-ui-onetag.git"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/fengyiqicoder/assistant-ui-onetag/issues"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@assistant-ui/react": ">=0.10.0",
|
|
56
|
+
"react": "^18 || ^19"
|
|
57
|
+
},
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"registry": "https://registry.npmjs.org",
|
|
60
|
+
"access": "public"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@assistant-ui/react": "^0.15.36",
|
|
64
|
+
"@testing-library/react": "^16.3.3",
|
|
65
|
+
"@types/react": "^19.3.0",
|
|
66
|
+
"@types/react-dom": "^19.3.0",
|
|
67
|
+
"jsdom": "^29.1.1",
|
|
68
|
+
"react": "^19.3.1",
|
|
69
|
+
"react-dom": "^19.3.1",
|
|
70
|
+
"tsup": "^9.0.0",
|
|
71
|
+
"typescript": "^7.0.2",
|
|
72
|
+
"vitest": "^5.1.0"
|
|
73
|
+
}
|
|
74
|
+
}
|