@moraya/core 0.2.0 → 0.4.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/README.md +215 -41
- package/dist/ai/drivers/claude.d.ts +6 -0
- package/dist/ai/drivers/claude.js +229 -0
- package/dist/ai/drivers/claude.js.map +1 -0
- package/dist/ai/drivers/gemini.d.ts +6 -0
- package/dist/ai/drivers/gemini.js +212 -0
- package/dist/ai/drivers/gemini.js.map +1 -0
- package/dist/ai/drivers/index.d.ts +14 -0
- package/dist/ai/drivers/index.js +617 -0
- package/dist/ai/drivers/index.js.map +1 -0
- package/dist/ai/drivers/ollama.d.ts +8 -0
- package/dist/ai/drivers/ollama.js +158 -0
- package/dist/ai/drivers/ollama.js.map +1 -0
- package/dist/ai/drivers/openai.d.ts +7 -0
- package/dist/ai/drivers/openai.js +225 -0
- package/dist/ai/drivers/openai.js.map +1 -0
- package/dist/ai/drivers/tool-bridge.d.ts +37 -0
- package/dist/ai/drivers/tool-bridge.js +138 -0
- package/dist/ai/drivers/tool-bridge.js.map +1 -0
- package/dist/ai/drivers/types.d.ts +2 -0
- package/dist/ai/drivers/types.js +1 -0
- package/dist/ai/drivers/types.js.map +1 -0
- package/dist/ai/drivers/util.d.ts +13 -0
- package/dist/ai/drivers/util.js +40 -0
- package/dist/ai/drivers/util.js.map +1 -0
- package/dist/ai/image.d.ts +37 -0
- package/dist/ai/image.js +36 -0
- package/dist/ai/image.js.map +1 -0
- package/dist/ai/index.d.ts +37 -0
- package/dist/ai/index.js +826 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/types.d.ts +92 -0
- package/dist/ai/types.js +1 -0
- package/dist/ai/types.js.map +1 -0
- package/dist/ai/voice.d.ts +42 -0
- package/dist/ai/voice.js +34 -0
- package/dist/ai/voice.js.map +1 -0
- package/dist/chat-markdown/index.d.ts +82 -0
- package/dist/chat-markdown/index.js +165 -0
- package/dist/chat-markdown/index.js.map +1 -0
- package/dist/i18n/locales/ar.json +806 -732
- package/dist/i18n/locales/de.json +912 -838
- package/dist/i18n/locales/en.json +34 -5
- package/dist/i18n/locales/es.json +952 -876
- package/dist/i18n/locales/fr.json +1784 -1708
- package/dist/i18n/locales/hi.json +1808 -1734
- package/dist/i18n/locales/ja.json +839 -765
- package/dist/i18n/locales/ko.json +1783 -1709
- package/dist/i18n/locales/pt.json +894 -820
- package/dist/i18n/locales/ru.json +812 -738
- package/dist/i18n/locales/zh-CN.json +34 -5
- package/dist/i18n/locales/zh-Hant.json +1039 -965
- package/dist/types-CwM77g7u.d.ts +88 -0
- package/package.json +26 -2
package/README.md
CHANGED
|
@@ -1,23 +1,171 @@
|
|
|
1
1
|
# @moraya/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Markdown engine for editors and AI chat.**
|
|
4
|
+
> A framework-agnostic ESM library powering WYSIWYG markdown editing (ProseMirror) and AI-chat-bubble rendering (markdown-it + optional KaTeX/highlight.js, streaming-safe). Used in production by [Moraya](https://moraya.app) across desktop / web / mobile. Designed to embed into any TypeScript app — Svelte, React, Vue, vanilla, or Node SSR.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
```bash
|
|
7
|
+
pnpm i @moraya/core
|
|
8
|
+
# or: npm i @moraya/core
|
|
9
|
+
# or: yarn add @moraya/core
|
|
10
|
+
```
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
- See `CHANGELOG.md`
|
|
12
|
+
---
|
|
9
13
|
|
|
10
|
-
##
|
|
14
|
+
## Why @moraya/core for AI apps
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
LLMs emit markdown. Your chat UI has to render it — safely, in real time, on every device.
|
|
13
17
|
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
18
|
+
- **Streaming-safe** — half-written code fences, math, and links from SSE chunks render gracefully without ever throwing. Every render is idempotent.
|
|
19
|
+
- **Security-first defaults** — `html: false`, JS/VBS/data: URL denylist, forced `rel="noopener noreferrer"` on every `<a>`. The same lockdown the Moraya mobile app ships.
|
|
20
|
+
- **Tree-shakeable math + highlighting** — KaTeX and highlight.js are wired by callback. Apps that don't need math (mobile chat, customer-support widgets) don't pay ~280 KB for it.
|
|
21
|
+
- **Shared editor schema** — when you eventually need a WYSIWYG editor in the same product, the rendering and the editor speak the same markdown dialect. No parser drift.
|
|
22
|
+
- **Pure ESM, peer-dep model** — works in Vite, esbuild, Rollup, webpack, Bun, Deno, Cloudflare Workers, Vercel Edge, Node SSR.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Quick start (AI chat bubble)
|
|
27
|
+
|
|
28
|
+
Five lines plus your LLM call:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { renderChatMarkdown } from '@moraya/core/chat-markdown'
|
|
32
|
+
|
|
33
|
+
const llmReply = await callYourLLM(prompt) // string
|
|
34
|
+
const safeHtml = renderChatMarkdown(llmReply) // safe HTML
|
|
35
|
+
bubbleEl.innerHTML = safeHtml // or React `dangerouslySetInnerHTML`, Svelte `{@html}`, Vue `v-html`
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
That's it. Code blocks, lists, headings, blockquotes, inline code, bold/italic, strikethrough, links — all rendered. Bare URLs are auto-linkified. Every `<a>` is sandboxed (`target="_blank" rel="noopener noreferrer"`).
|
|
39
|
+
|
|
40
|
+
### Adding syntax highlighting
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { renderChatMarkdown } from '@moraya/core/chat-markdown'
|
|
44
|
+
import hljs from 'highlight.js'
|
|
45
|
+
|
|
46
|
+
const html = renderChatMarkdown(reply, {
|
|
47
|
+
highlight: (code, lang) => {
|
|
48
|
+
if (lang && hljs.getLanguage(lang)) {
|
|
49
|
+
return hljs.highlight(code, { language: lang, ignoreIllegals: true }).value
|
|
50
|
+
}
|
|
51
|
+
return null // fall back to default escaped <code>
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Adding KaTeX math
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import katex from 'katex'
|
|
60
|
+
import 'katex/dist/katex.min.css'
|
|
61
|
+
|
|
62
|
+
const html = renderChatMarkdown(reply, {
|
|
63
|
+
math: (latex, displayMode) =>
|
|
64
|
+
katex.renderToString(latex, { displayMode, throwOnError: false }),
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`$inline$` and `$$display$$` are parsed; everything else (including dollar amounts like `$5`) is left alone.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Use cases
|
|
73
|
+
|
|
74
|
+
| Use case | Subpath | Bundle size (gzipped) |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| **AI chat bubbles** — render LLM markdown output | `@moraya/core/chat-markdown` | ~7 KB + your `markdown-it` peer |
|
|
77
|
+
| **Embedded WYSIWYG editor** — Typora-style markdown editing | `@moraya/core` (+ schema/setup/commands subpaths) | ~80 KB + ProseMirror peers |
|
|
78
|
+
| **Server-side markdown → HTML** — Node SSR, Edge, Workers | `@moraya/core/chat-markdown` (no DOM needed) | ~7 KB |
|
|
79
|
+
| **Provider-agnostic LLM client** — chat completion + tool calls | `@moraya/core/ai` | ~15 KB |
|
|
80
|
+
|
|
81
|
+
You can use any of these independently. The `chat-markdown` subpath has no dependency on the editor, ProseMirror, or DOM.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Framework examples
|
|
86
|
+
|
|
87
|
+
### Vanilla TypeScript (no framework)
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { renderChatMarkdown } from '@moraya/core/chat-markdown'
|
|
91
|
+
|
|
92
|
+
const el = document.querySelector('#chat')!
|
|
93
|
+
el.innerHTML = renderChatMarkdown(`
|
|
94
|
+
**Hello!** Here's some code:
|
|
95
|
+
\`\`\`python
|
|
96
|
+
def greet(name): return f"Hi, {name}!"
|
|
97
|
+
\`\`\`
|
|
98
|
+
`)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### React
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { renderChatMarkdown } from '@moraya/core/chat-markdown'
|
|
105
|
+
|
|
106
|
+
export function ChatBubble({ content }: { content: string }) {
|
|
107
|
+
const html = renderChatMarkdown(content)
|
|
108
|
+
return <div dangerouslySetInnerHTML={{ __html: html }} />
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Svelte 5
|
|
113
|
+
|
|
114
|
+
```svelte
|
|
115
|
+
<script lang="ts">
|
|
116
|
+
import { renderChatMarkdown } from '@moraya/core/chat-markdown'
|
|
117
|
+
let { content }: { content: string } = $props()
|
|
118
|
+
const html = $derived(renderChatMarkdown(content))
|
|
119
|
+
</script>
|
|
19
120
|
|
|
20
|
-
|
|
121
|
+
<div>{@html html}</div>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Vue 3
|
|
125
|
+
|
|
126
|
+
```vue
|
|
127
|
+
<script setup lang="ts">
|
|
128
|
+
import { computed } from 'vue'
|
|
129
|
+
import { renderChatMarkdown } from '@moraya/core/chat-markdown'
|
|
130
|
+
const props = defineProps<{ content: string }>()
|
|
131
|
+
const html = computed(() => renderChatMarkdown(props.content))
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<template>
|
|
135
|
+
<div v-html="html" />
|
|
136
|
+
</template>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Runnable demos live in [`examples/`](./examples/).
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## API reference
|
|
144
|
+
|
|
145
|
+
### `chat-markdown` subpath
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { renderChatMarkdown } from '@moraya/core/chat-markdown'
|
|
149
|
+
|
|
150
|
+
renderChatMarkdown(input: string, opts?: ChatMarkdownOptions): string
|
|
151
|
+
|
|
152
|
+
interface ChatMarkdownOptions {
|
|
153
|
+
/** KaTeX math callback. When omitted, $...$ syntax renders as plain text. */
|
|
154
|
+
math?: (latex: string, displayMode: boolean) => string
|
|
155
|
+
|
|
156
|
+
/** Syntax-highlighter callback. When omitted, code blocks render with
|
|
157
|
+
* HTML-escaped content inside <pre><code>. Return null to fall back. */
|
|
158
|
+
highlight?: (code: string, lang: string) => string | null
|
|
159
|
+
|
|
160
|
+
/** Override default link attributes. Defaults: target=_blank, rel=noopener noreferrer. */
|
|
161
|
+
linkAttrs?: { target?: string; rel?: string }
|
|
162
|
+
|
|
163
|
+
/** Pre-process the markdown before parsing (mentions, slash-commands, etc). */
|
|
164
|
+
preprocess?: (raw: string) => string
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Editor subpaths
|
|
21
169
|
|
|
22
170
|
```ts
|
|
23
171
|
import {
|
|
@@ -27,56 +175,82 @@ import {
|
|
|
27
175
|
parseMarkdown, parseMarkdownAsync, serializeMarkdown,
|
|
28
176
|
// setup
|
|
29
177
|
createEditor, createEditorPlugins,
|
|
30
|
-
type EditorPluginOptions, type CreateEditorOptions, type MorayaEditorInstance,
|
|
31
|
-
// doc cache
|
|
32
|
-
createDocCache, type DocCache,
|
|
33
178
|
// commands
|
|
34
179
|
toggleBold, toggleItalic, toggleStrikethrough, toggleCode,
|
|
35
180
|
setHeading, toggleBlockquote, toggleOrderedList, toggleBulletList, toggleCodeBlock,
|
|
36
|
-
insertTable, insertHorizontalRule, insertMathBlock,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
181
|
+
insertTable, insertHorizontalRule, insertMathBlock, toggleLink, insertImage,
|
|
182
|
+
// doc cache
|
|
183
|
+
createDocCache, type DocCache,
|
|
184
|
+
// DI types
|
|
185
|
+
type MediaResolver, type LinkOpener, type RendererRegistry, type Platform,
|
|
40
186
|
} from '@moraya/core'
|
|
41
187
|
|
|
42
188
|
import { BrowserMediaResolver } from '@moraya/core/adapters/browser-media-resolver'
|
|
43
189
|
import '@moraya/core/style'
|
|
44
190
|
```
|
|
45
191
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
### Moraya desktop bridge (Svelte 5 + Tauri)
|
|
192
|
+
### AI provider layer
|
|
49
193
|
|
|
50
194
|
```ts
|
|
51
|
-
import {
|
|
52
|
-
import { tauriMediaResolver } from './tauri-media-resolver'
|
|
53
|
-
import { morayaRendererRegistry } from './moraya-renderer-registry'
|
|
54
|
-
|
|
55
|
-
export const schema = createSchema({
|
|
56
|
-
mediaResolver: tauriMediaResolver,
|
|
57
|
-
rendererRegistry: morayaRendererRegistry,
|
|
58
|
-
})
|
|
195
|
+
import { streamChat, sendChat, type AITransport } from '@moraya/core/ai'
|
|
59
196
|
```
|
|
60
197
|
|
|
61
|
-
|
|
198
|
+
See [`src/ai/index.ts`](./src/ai/index.ts) for the full surface.
|
|
62
199
|
|
|
63
|
-
|
|
64
|
-
import { createSchema } from '@moraya/core'
|
|
65
|
-
import { BrowserMediaResolver } from '@moraya/core/adapters/browser-media-resolver'
|
|
200
|
+
---
|
|
66
201
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
202
|
+
## Comparison
|
|
203
|
+
|
|
204
|
+
| | @moraya/core/chat-markdown | bare markdown-it | react-markdown | marked |
|
|
205
|
+
|---|---|---|---|---|
|
|
206
|
+
| Streaming-safe (LLM SSE) | ✅ never throws | ⚠️ depends on plugins | ⚠️ React reconciliation cost | ⚠️ |
|
|
207
|
+
| `html: false` by default | ✅ | ❌ must set explicitly | ✅ | ❌ |
|
|
208
|
+
| `rel="noopener"` forced | ✅ | ❌ DIY plugin | ❌ DIY | ❌ |
|
|
209
|
+
| `javascript:` / `data:text/html` denied | ✅ | ⚠️ default but bypassable | ⚠️ | ❌ |
|
|
210
|
+
| KaTeX as opt-in callback | ✅ | ⚠️ via texmath plugin | ⚠️ via remark-math | ❌ |
|
|
211
|
+
| Tree-shakeable highlighter | ✅ | ⚠️ wires hljs directly | ⚠️ via rehype-highlight | ❌ |
|
|
212
|
+
| Pure ESM | ✅ | ✅ | ⚠️ CJS+ESM | ✅ |
|
|
213
|
+
| Works in Edge / Workers / SSR | ✅ | ✅ | ❌ needs React | ✅ |
|
|
214
|
+
| Bundle size (gzipped) | ~7 KB | ~25 KB | ~30 KB + React | ~10 KB |
|
|
215
|
+
| Shares schema with full editor | ✅ (via `@moraya/core/markdown`) | ❌ | ❌ | ❌ |
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Design constraints
|
|
220
|
+
|
|
221
|
+
- **Pure ESM** — no CommonJS, no Node API, no host-specific imports
|
|
222
|
+
- **ES2022** target (compatible with iOS 14+ Safari, Android 8+ Chrome, Node 20+, modern Edge)
|
|
223
|
+
- **DOM-optional** — `chat-markdown` is pure string→string; the editor parts require DOM Level 4 + ContentEditable
|
|
224
|
+
- **Dependency-injected** — `MediaResolver` / `LinkOpener` / `RendererRegistry` / `Platform` for the editor; callbacks for math/highlight in chat-markdown
|
|
225
|
+
- **Peer-dep model** — `prosemirror-*`, `markdown-it`, `katex`, `highlight.js` are peers; the consumer's bundler decides what ships
|
|
226
|
+
- **Bundle budget** — main editor entry ≤ 80 KB gzipped; chat-markdown ≤ 8 KB gzipped
|
|
227
|
+
|
|
228
|
+
See [v0.40.0 iteration](https://github.com/zouwei/moraya/blob/main/docs/iterations/v0.40.0-core-shared-markdown-core.md) for the full design contract.
|
|
229
|
+
|
|
230
|
+
---
|
|
71
231
|
|
|
72
232
|
## Build & test
|
|
73
233
|
|
|
74
234
|
```bash
|
|
75
235
|
pnpm install
|
|
76
|
-
pnpm build
|
|
77
|
-
pnpm test
|
|
236
|
+
pnpm build # tsup → dist/ ESM + .d.ts
|
|
237
|
+
pnpm test # vitest (217+ tests)
|
|
238
|
+
pnpm typecheck # tsc --noEmit
|
|
78
239
|
```
|
|
79
240
|
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Production users
|
|
244
|
+
|
|
245
|
+
- **[Moraya](https://moraya.app)** — minimalist WYSIWYG markdown editor; uses every subpath
|
|
246
|
+
- Desktop (Tauri 2 + Svelte 5)
|
|
247
|
+
- Web ([moraya.app](https://moraya.app), SvelteKit SPA)
|
|
248
|
+
- Mobile (Capacitor + Svelte 5, iOS + Android)
|
|
249
|
+
|
|
250
|
+
Built something with `@moraya/core`? Open a PR to add yourself.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
80
254
|
## License
|
|
81
255
|
|
|
82
|
-
|
|
256
|
+
See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// src/ai/drivers/tool-bridge.ts
|
|
2
|
+
var GEMINI_UNSUPPORTED_KEYS = /* @__PURE__ */ new Set([
|
|
3
|
+
"additionalProperties",
|
|
4
|
+
"$schema",
|
|
5
|
+
"$id",
|
|
6
|
+
"$ref",
|
|
7
|
+
"$defs",
|
|
8
|
+
"definitions",
|
|
9
|
+
"patternProperties",
|
|
10
|
+
"unevaluatedProperties",
|
|
11
|
+
"dependentRequired",
|
|
12
|
+
"dependentSchemas",
|
|
13
|
+
"const"
|
|
14
|
+
]);
|
|
15
|
+
function sanitizeGeminiSchema(schema) {
|
|
16
|
+
if (Array.isArray(schema)) return schema.map(sanitizeGeminiSchema);
|
|
17
|
+
if (schema && typeof schema === "object") {
|
|
18
|
+
const out = {};
|
|
19
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
20
|
+
if (GEMINI_UNSUPPORTED_KEYS.has(key)) continue;
|
|
21
|
+
out[key] = sanitizeGeminiSchema(value);
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
return schema;
|
|
26
|
+
}
|
|
27
|
+
function formatToolsForProvider(provider, tools) {
|
|
28
|
+
if (tools.length === 0) return {};
|
|
29
|
+
switch (provider) {
|
|
30
|
+
case "claude":
|
|
31
|
+
return {
|
|
32
|
+
tools: tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.input_schema }))
|
|
33
|
+
};
|
|
34
|
+
case "gemini":
|
|
35
|
+
return {
|
|
36
|
+
tools: [{
|
|
37
|
+
functionDeclarations: tools.map((t) => ({
|
|
38
|
+
name: t.name,
|
|
39
|
+
description: t.description,
|
|
40
|
+
parameters: sanitizeGeminiSchema(t.input_schema)
|
|
41
|
+
}))
|
|
42
|
+
}]
|
|
43
|
+
};
|
|
44
|
+
default:
|
|
45
|
+
return {
|
|
46
|
+
tools: tools.map((t) => ({
|
|
47
|
+
type: "function",
|
|
48
|
+
function: { name: t.name, description: t.description, parameters: t.input_schema }
|
|
49
|
+
}))
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function parseClaudeToolCalls(data) {
|
|
54
|
+
const content = data.content;
|
|
55
|
+
const stopReason = data.stop_reason || "end_turn";
|
|
56
|
+
const toolCalls = [];
|
|
57
|
+
let textContent = "";
|
|
58
|
+
if (content) {
|
|
59
|
+
for (const block of content) {
|
|
60
|
+
if (block.type === "tool_use") {
|
|
61
|
+
toolCalls.push({ id: block.id, name: block.name, arguments: block.input || {} });
|
|
62
|
+
} else if (block.type === "text") {
|
|
63
|
+
textContent += block.text;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { toolCalls, textContent, stopReason };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/ai/catalog.ts
|
|
71
|
+
var PROVIDER_BASE_URLS = {
|
|
72
|
+
claude: "https://api.anthropic.com",
|
|
73
|
+
openai: "https://api.openai.com",
|
|
74
|
+
gemini: "https://generativelanguage.googleapis.com",
|
|
75
|
+
deepseek: "https://api.deepseek.com",
|
|
76
|
+
ollama: "http://localhost:11434",
|
|
77
|
+
grok: "https://api.x.ai",
|
|
78
|
+
mistral: "https://api.mistral.ai",
|
|
79
|
+
glm: "https://open.bigmodel.cn/api/paas/v4",
|
|
80
|
+
minimax: "https://api.minimax.io/v1",
|
|
81
|
+
doubao: "https://ark.cn-beijing.volces.com/api/v3",
|
|
82
|
+
custom: "",
|
|
83
|
+
"local-mlx": "",
|
|
84
|
+
"local-llama": ""
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// src/ai/drivers/util.ts
|
|
88
|
+
function resolveBaseUrl(config, fallback) {
|
|
89
|
+
return config.baseUrl || PROVIDER_BASE_URLS[config.provider] || fallback;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/ai/drivers/claude.ts
|
|
93
|
+
function buildClaudeMessages(messages) {
|
|
94
|
+
const result = [];
|
|
95
|
+
for (const msg of messages) {
|
|
96
|
+
if (msg.role === "assistant" && msg.toolCalls && msg.toolCalls.length > 0) {
|
|
97
|
+
const content = [];
|
|
98
|
+
if (msg.content) content.push({ type: "text", text: msg.content });
|
|
99
|
+
for (const tc of msg.toolCalls) content.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc.arguments });
|
|
100
|
+
result.push({ role: "assistant", content });
|
|
101
|
+
} else if (msg.role === "tool") {
|
|
102
|
+
const lastMsg = result[result.length - 1];
|
|
103
|
+
const toolResultBlock = { type: "tool_result", tool_use_id: msg.toolCallId, content: msg.content, is_error: msg.isError || false };
|
|
104
|
+
if (lastMsg && lastMsg.role === "user" && Array.isArray(lastMsg.content) && lastMsg.content.every((b) => b.type === "tool_result")) {
|
|
105
|
+
lastMsg.content.push(toolResultBlock);
|
|
106
|
+
} else {
|
|
107
|
+
result.push({ role: "user", content: [toolResultBlock] });
|
|
108
|
+
}
|
|
109
|
+
} else if (msg.role === "user" && msg.images && msg.images.length > 0) {
|
|
110
|
+
const content = [];
|
|
111
|
+
for (const img of msg.images) content.push({ type: "image", source: { type: "base64", media_type: img.mimeType, data: img.base64 } });
|
|
112
|
+
if (msg.content) content.push({ type: "text", text: msg.content });
|
|
113
|
+
result.push({ role: "user", content });
|
|
114
|
+
} else {
|
|
115
|
+
result.push({ role: msg.role, content: msg.content });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
var claudeDriver = {
|
|
121
|
+
supportsStreaming: true,
|
|
122
|
+
buildChatRequest(config, request, stream) {
|
|
123
|
+
const baseUrl = resolveBaseUrl(config, "https://api.anthropic.com");
|
|
124
|
+
const systemMessages = request.messages.filter((m) => m.role === "system");
|
|
125
|
+
const chatMessages = request.messages.filter((m) => m.role !== "system");
|
|
126
|
+
const body = {
|
|
127
|
+
model: request.model || config.model,
|
|
128
|
+
max_tokens: request.maxTokens ?? config.maxTokens ?? 41920,
|
|
129
|
+
messages: buildClaudeMessages(chatMessages)
|
|
130
|
+
};
|
|
131
|
+
if (stream) body.stream = true;
|
|
132
|
+
if (systemMessages.length > 0) body.system = systemMessages.map((m) => m.content).join("\n");
|
|
133
|
+
const temperature = request.temperature ?? config.temperature;
|
|
134
|
+
if (temperature !== void 0) body.temperature = temperature;
|
|
135
|
+
const topP = request.topP ?? config.topP;
|
|
136
|
+
if (topP !== void 0) body.top_p = topP;
|
|
137
|
+
if (request.stop && request.stop.length > 0) body.stop_sequences = request.stop.slice(0, 5);
|
|
138
|
+
if (request.tools && request.tools.length > 0) Object.assign(body, formatToolsForProvider("claude", request.tools));
|
|
139
|
+
return {
|
|
140
|
+
provider: "claude",
|
|
141
|
+
configId: config.id,
|
|
142
|
+
method: "POST",
|
|
143
|
+
url: `${baseUrl}/v1/messages`,
|
|
144
|
+
headers: {
|
|
145
|
+
"anthropic-version": "2023-06-01",
|
|
146
|
+
// Required for direct browser fetch; harmless when proxied via Rust.
|
|
147
|
+
"anthropic-dangerous-direct-browser-access": "true"
|
|
148
|
+
},
|
|
149
|
+
body: JSON.stringify(body),
|
|
150
|
+
auth: { scheme: "header", headerName: "x-api-key" }
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
parseResponse(json, _config) {
|
|
154
|
+
const parsed = parseClaudeToolCalls(json);
|
|
155
|
+
const usage = json.usage;
|
|
156
|
+
return {
|
|
157
|
+
content: parsed.textContent,
|
|
158
|
+
model: json.model || _config.model,
|
|
159
|
+
usage: { inputTokens: usage?.input_tokens || 0, outputTokens: usage?.output_tokens || 0 },
|
|
160
|
+
...parsed.toolCalls.length > 0 ? { toolCalls: parsed.toolCalls } : {},
|
|
161
|
+
stopReason: parsed.stopReason
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
createStreamFold() {
|
|
165
|
+
const partials = /* @__PURE__ */ new Map();
|
|
166
|
+
const toolCalls = [];
|
|
167
|
+
let stopReason = "end_turn";
|
|
168
|
+
let inputTokens = 0;
|
|
169
|
+
let outputTokens = 0;
|
|
170
|
+
return {
|
|
171
|
+
pushEnvelope(raw) {
|
|
172
|
+
let v;
|
|
173
|
+
try {
|
|
174
|
+
v = JSON.parse(raw);
|
|
175
|
+
} catch {
|
|
176
|
+
return void 0;
|
|
177
|
+
}
|
|
178
|
+
switch (v.type) {
|
|
179
|
+
case "message_start": {
|
|
180
|
+
const u = v.message?.usage;
|
|
181
|
+
if (u?.input_tokens) inputTokens = u.input_tokens;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
case "content_block_delta": {
|
|
185
|
+
const delta = v.delta;
|
|
186
|
+
if (delta?.type === "text_delta") return delta.text || void 0;
|
|
187
|
+
if (delta?.type === "input_json_delta") {
|
|
188
|
+
const p = partials.get(v.index);
|
|
189
|
+
if (p) p.json += delta.partial_json || "";
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case "content_block_start": {
|
|
194
|
+
const block = v.content_block;
|
|
195
|
+
if (block?.type === "tool_use") partials.set(v.index, { id: block.id, name: block.name, json: "" });
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
case "content_block_stop": {
|
|
199
|
+
const p = partials.get(v.index);
|
|
200
|
+
if (p) {
|
|
201
|
+
try {
|
|
202
|
+
toolCalls.push({ id: p.id, name: p.name, arguments: JSON.parse(p.json || "{}") });
|
|
203
|
+
} catch {
|
|
204
|
+
}
|
|
205
|
+
partials.delete(v.index);
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case "message_delta": {
|
|
210
|
+
const d = v.delta;
|
|
211
|
+
if (d?.stop_reason) stopReason = d.stop_reason === "tool_use" ? "tool_use" : d.stop_reason;
|
|
212
|
+
const u = v.usage;
|
|
213
|
+
if (u?.output_tokens) outputTokens = u.output_tokens;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return void 0;
|
|
218
|
+
},
|
|
219
|
+
finish() {
|
|
220
|
+
const usage = inputTokens || outputTokens ? { inputTokens, outputTokens } : void 0;
|
|
221
|
+
return { ...toolCalls.length > 0 ? { toolCalls } : {}, stopReason, ...usage ? { usage } : {} };
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
export {
|
|
227
|
+
claudeDriver
|
|
228
|
+
};
|
|
229
|
+
//# sourceMappingURL=claude.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/ai/drivers/tool-bridge.ts","../../../src/ai/catalog.ts","../../../src/ai/drivers/util.ts","../../../src/ai/drivers/claude.ts"],"sourcesContent":["/**\n * Tool formatting + response parsing, shared by all consumers.\n * Ported verbatim from the desktop app's tool-bridge (MCP-specific glue like\n * `mcpToolsToToolDefs` stays in the desktop repo — it has no place in core).\n */\nimport type { AIProvider, ToolDefinition, ToolCallRequest } from '../types'\n\nconst GEMINI_UNSUPPORTED_KEYS = new Set([\n 'additionalProperties', '$schema', '$id', '$ref', '$defs', 'definitions',\n 'patternProperties', 'unevaluatedProperties', 'dependentRequired',\n 'dependentSchemas', 'const',\n])\n\nfunction sanitizeGeminiSchema(schema: unknown): unknown {\n if (Array.isArray(schema)) return schema.map(sanitizeGeminiSchema)\n if (schema && typeof schema === 'object') {\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(schema as Record<string, unknown>)) {\n if (GEMINI_UNSUPPORTED_KEYS.has(key)) continue\n out[key] = sanitizeGeminiSchema(value)\n }\n return out\n }\n return schema\n}\n\n/** Format tools into provider-specific request-body fields (merge into body). */\nexport function formatToolsForProvider(\n provider: AIProvider,\n tools: ToolDefinition[],\n): Record<string, unknown> {\n if (tools.length === 0) return {}\n switch (provider) {\n case 'claude':\n return {\n tools: tools.map(t => ({ name: t.name, description: t.description, input_schema: t.input_schema })),\n }\n case 'gemini':\n return {\n tools: [{\n functionDeclarations: tools.map(t => ({\n name: t.name,\n description: t.description,\n parameters: sanitizeGeminiSchema(t.input_schema),\n })),\n }],\n }\n default:\n // OpenAI-compatible (openai/deepseek/grok/mistral/glm/minimax/doubao/custom/ollama)\n return {\n tools: tools.map(t => ({\n type: 'function',\n function: { name: t.name, description: t.description, parameters: t.input_schema },\n })),\n }\n }\n}\n\nexport function parseClaudeToolCalls(data: Record<string, unknown>): {\n toolCalls: ToolCallRequest[]; textContent: string; stopReason: string\n} {\n const content = data.content as Array<Record<string, unknown>> | undefined\n const stopReason = (data.stop_reason as string) || 'end_turn'\n const toolCalls: ToolCallRequest[] = []\n let textContent = ''\n if (content) {\n for (const block of content) {\n if (block.type === 'tool_use') {\n toolCalls.push({ id: block.id as string, name: block.name as string, arguments: (block.input as Record<string, unknown>) || {} })\n } else if (block.type === 'text') {\n textContent += block.text as string\n }\n }\n }\n return { toolCalls, textContent, stopReason }\n}\n\nexport function parseOpenAIToolCalls(data: Record<string, unknown>): {\n toolCalls: ToolCallRequest[]; textContent: string; stopReason: string\n} {\n const choices = data.choices as Array<Record<string, unknown>> | undefined\n if (!choices || choices.length === 0) return { toolCalls: [], textContent: '', stopReason: 'stop' }\n const choice = choices[0]!\n const message = choice.message as Record<string, unknown> | undefined\n const finishReason = (choice.finish_reason as string) || 'stop'\n const textContent = (message?.content as string) || ''\n const toolCalls: ToolCallRequest[] = []\n const rawToolCalls = message?.tool_calls as Array<Record<string, unknown>> | undefined\n if (rawToolCalls) {\n for (const tc of rawToolCalls) {\n const fn = tc.function as Record<string, unknown>\n let args: Record<string, unknown> = {}\n try { args = JSON.parse(fn.arguments as string) } catch { /* truncated */ }\n toolCalls.push({ id: tc.id as string, name: fn.name as string, arguments: args })\n }\n }\n return { toolCalls, textContent, stopReason: finishReason === 'tool_calls' ? 'tool_use' : finishReason }\n}\n\nexport function parseGeminiToolCalls(data: Record<string, unknown>): {\n toolCalls: ToolCallRequest[]; textContent: string; stopReason: string\n} {\n const candidates = data.candidates as Array<Record<string, unknown>> | undefined\n if (!candidates || candidates.length === 0) return { toolCalls: [], textContent: '', stopReason: 'stop' }\n const content = candidates[0]!.content as Record<string, unknown> | undefined\n const parts = content?.parts as Array<Record<string, unknown>> | undefined\n const toolCalls: ToolCallRequest[] = []\n let textContent = ''\n if (parts) {\n for (const part of parts) {\n if (part.functionCall) {\n const fc = part.functionCall as Record<string, unknown>\n const thoughtSignature =\n (part.thoughtSignature as string | undefined) ?? (fc.thoughtSignature as string | undefined)\n toolCalls.push({\n id: `gemini-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n name: fc.name as string,\n arguments: (fc.args as Record<string, unknown>) || {},\n ...(thoughtSignature ? { providerMeta: { thoughtSignature } } : {}),\n })\n } else if (part.text) {\n textContent += part.text as string\n }\n }\n }\n return { toolCalls, textContent, stopReason: toolCalls.length > 0 ? 'tool_use' : 'stop' }\n}\n\nexport function buildClaudeToolResultMessages(\n toolResults: Array<{ callId: string; content: string; isError?: boolean }>,\n): Record<string, unknown> {\n return {\n role: 'user',\n content: toolResults.map(r => ({\n type: 'tool_result', tool_use_id: r.callId, content: r.content, is_error: r.isError || false,\n })),\n }\n}\n\nexport function buildOpenAIToolResultMessages(\n toolResults: Array<{ callId: string; name: string; content: string }>,\n): Array<Record<string, unknown>> {\n return toolResults.map(r => ({ role: 'tool', tool_call_id: r.callId, content: r.content }))\n}\n","/**\n * Provider catalog — default models, base URLs, labels, id aliases.\n * Baselined on the desktop app. Pure data; no host imports.\n */\nimport type { AIProvider } from './types'\n\nexport const DEFAULT_MODELS: Record<AIProvider, string[]> = {\n claude: ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001'],\n openai: ['gpt-5.2', 'gpt-5.2-pro', 'gpt-5', 'gpt-5-mini', 'o4-mini', 'gpt-4o', 'gpt-4o-mini', 'o3', 'o3-mini'],\n gemini: ['gemini-3.1-pro-preview', 'gemini-3-flash-preview', 'gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.0-pro-exp'],\n deepseek: ['deepseek-chat', 'deepseek-reasoner'],\n ollama: ['llama3.3', 'llama3.2', 'qwen2.5', 'qwen2.5-coder', 'phi4', 'gemma3', 'deepseek-r1', 'mistral', 'codellama'],\n grok: ['grok-4', 'grok-4-1-fast-reasoning', 'grok-4-1-fast-non-reasoning', 'grok-code-fast-1', 'grok-3'],\n mistral: ['mistral-large-latest', 'mistral-small-latest', 'magistral-medium-latest', 'magistral-small-latest', 'codestral-latest', 'devstral-latest'],\n glm: ['glm-5', 'glm-4-plus', 'glm-4-air', 'glm-4-flash', 'glm-z1-flash', 'glm-z1-air'],\n minimax: ['MiniMax-M2.5', 'MiniMax-M2.5-highspeed', 'MiniMax-Text-01'],\n doubao: [],\n custom: [],\n 'local-mlx': ['Qwen2.5-1.5B-Instruct-4bit'],\n 'local-llama': ['qwen2.5-1.5b-instruct-q4'],\n}\n\nexport const PROVIDER_BASE_URLS: Record<AIProvider, string> = {\n claude: 'https://api.anthropic.com',\n openai: 'https://api.openai.com',\n gemini: 'https://generativelanguage.googleapis.com',\n deepseek: 'https://api.deepseek.com',\n ollama: 'http://localhost:11434',\n grok: 'https://api.x.ai',\n mistral: 'https://api.mistral.ai',\n glm: 'https://open.bigmodel.cn/api/paas/v4',\n minimax: 'https://api.minimax.io/v1',\n doubao: 'https://ark.cn-beijing.volces.com/api/v3',\n custom: '',\n 'local-mlx': '',\n 'local-llama': '',\n}\n\nexport const PROVIDER_LABELS: Record<AIProvider, string> = {\n claude: 'Claude',\n openai: 'OpenAI',\n gemini: 'Gemini',\n deepseek: 'DeepSeek',\n ollama: 'Ollama',\n grok: 'Grok',\n mistral: 'Mistral',\n glm: 'GLM',\n minimax: 'MiniMax',\n doubao: 'Doubao',\n custom: 'Custom (OpenAI-compatible)',\n 'local-mlx': 'On-device (iOS)',\n 'local-llama': 'On-device (Android)',\n}\n\n/** Legacy/foreign provider ids → canonical `AIProvider`. */\nexport const PROVIDER_ALIASES: Record<string, AIProvider> = {\n anthropic: 'claude',\n}\n\n/** Normalize an incoming provider id (applies aliases). */\nexport function normalizeProvider(id: string): AIProvider {\n return (PROVIDER_ALIASES[id] ?? id) as AIProvider\n}\n","import { PROVIDER_BASE_URLS } from '../catalog'\nimport type { AIProviderConfig } from '../types'\n\nexport function resolveBaseUrl(config: AIProviderConfig, fallback: string): string {\n return config.baseUrl || PROVIDER_BASE_URLS[config.provider] || fallback\n}\n\n/** Build an OpenAI-compatible endpoint, avoiding a double version prefix. */\nexport function openaiEndpoint(baseUrl: string, path: string): string {\n const clean = baseUrl.replace(/\\/+$/, '')\n if (/\\/v\\d+$/.test(clean)) return `${clean}${path}`\n return `${clean}/v1${path}`\n}\n\nexport const NOOP_FOLD = {\n pushEnvelope() { return undefined },\n finish() { return { stopReason: 'end_turn' } },\n}\n","import type { AIProviderConfig, AIRequest, AIResponse, ChatMessage, ToolCallRequest } from '../types'\nimport type { TransportRequest } from '../transport'\nimport type { AIDriver, StreamFold } from './types'\nimport { formatToolsForProvider, parseClaudeToolCalls } from './tool-bridge'\nimport { resolveBaseUrl } from './util'\n\nfunction buildClaudeMessages(messages: ChatMessage[]): Array<Record<string, unknown>> {\n const result: Array<Record<string, unknown>> = []\n for (const msg of messages) {\n if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {\n const content: Array<Record<string, unknown>> = []\n if (msg.content) content.push({ type: 'text', text: msg.content })\n for (const tc of msg.toolCalls) content.push({ type: 'tool_use', id: tc.id, name: tc.name, input: tc.arguments })\n result.push({ role: 'assistant', content })\n } else if (msg.role === 'tool') {\n const lastMsg = result[result.length - 1]\n const toolResultBlock = { type: 'tool_result', tool_use_id: msg.toolCallId, content: msg.content, is_error: msg.isError || false }\n if (lastMsg && lastMsg.role === 'user' && Array.isArray(lastMsg.content) &&\n (lastMsg.content as Array<Record<string, unknown>>).every(b => b.type === 'tool_result')) {\n (lastMsg.content as Array<Record<string, unknown>>).push(toolResultBlock)\n } else {\n result.push({ role: 'user', content: [toolResultBlock] })\n }\n } else if (msg.role === 'user' && msg.images && msg.images.length > 0) {\n const content: Array<Record<string, unknown>> = []\n for (const img of msg.images) content.push({ type: 'image', source: { type: 'base64', media_type: img.mimeType, data: img.base64 } })\n if (msg.content) content.push({ type: 'text', text: msg.content })\n result.push({ role: 'user', content })\n } else {\n result.push({ role: msg.role, content: msg.content })\n }\n }\n return result\n}\n\nexport const claudeDriver: AIDriver = {\n supportsStreaming: true,\n\n buildChatRequest(config, request, stream): TransportRequest {\n const baseUrl = resolveBaseUrl(config, 'https://api.anthropic.com')\n const systemMessages = request.messages.filter(m => m.role === 'system')\n const chatMessages = request.messages.filter(m => m.role !== 'system')\n\n const body: Record<string, unknown> = {\n model: request.model || config.model,\n max_tokens: request.maxTokens ?? config.maxTokens ?? 41920,\n messages: buildClaudeMessages(chatMessages),\n }\n if (stream) body.stream = true\n if (systemMessages.length > 0) body.system = systemMessages.map(m => m.content).join('\\n')\n const temperature = request.temperature ?? config.temperature\n if (temperature !== undefined) body.temperature = temperature\n const topP = request.topP ?? config.topP\n if (topP !== undefined) body.top_p = topP\n // Anthropic caps stop_sequences (~5) — trim quietly.\n if (request.stop && request.stop.length > 0) body.stop_sequences = request.stop.slice(0, 5)\n if (request.tools && request.tools.length > 0) Object.assign(body, formatToolsForProvider('claude', request.tools))\n\n return {\n provider: 'claude',\n configId: config.id,\n method: 'POST',\n url: `${baseUrl}/v1/messages`,\n headers: {\n 'anthropic-version': '2023-06-01',\n // Required for direct browser fetch; harmless when proxied via Rust.\n 'anthropic-dangerous-direct-browser-access': 'true',\n },\n body: JSON.stringify(body),\n auth: { scheme: 'header', headerName: 'x-api-key' },\n }\n },\n\n parseResponse(json, _config): AIResponse {\n const parsed = parseClaudeToolCalls(json)\n const usage = json.usage as Record<string, number> | undefined\n return {\n content: parsed.textContent,\n model: (json.model as string) || _config.model,\n usage: { inputTokens: usage?.input_tokens || 0, outputTokens: usage?.output_tokens || 0 },\n ...(parsed.toolCalls.length > 0 ? { toolCalls: parsed.toolCalls } : {}),\n stopReason: parsed.stopReason,\n }\n },\n\n createStreamFold(): StreamFold {\n const partials = new Map<number, { id: string; name: string; json: string }>()\n const toolCalls: ToolCallRequest[] = []\n let stopReason = 'end_turn'\n let inputTokens = 0\n let outputTokens = 0\n return {\n pushEnvelope(raw) {\n let v: Record<string, unknown>\n try { v = JSON.parse(raw) } catch { return undefined }\n switch (v.type as string) {\n case 'message_start': {\n const u = (v.message as Record<string, unknown> | undefined)?.usage as Record<string, number> | undefined\n if (u?.input_tokens) inputTokens = u.input_tokens\n break\n }\n case 'content_block_delta': {\n const delta = v.delta as Record<string, unknown> | undefined\n if (delta?.type === 'text_delta') return (delta.text as string) || undefined\n if (delta?.type === 'input_json_delta') {\n const p = partials.get(v.index as number)\n if (p) p.json += (delta.partial_json as string) || ''\n }\n break\n }\n case 'content_block_start': {\n const block = v.content_block as Record<string, unknown> | undefined\n if (block?.type === 'tool_use') partials.set(v.index as number, { id: block.id as string, name: block.name as string, json: '' })\n break\n }\n case 'content_block_stop': {\n const p = partials.get(v.index as number)\n if (p) {\n try { toolCalls.push({ id: p.id, name: p.name, arguments: JSON.parse(p.json || '{}') }) } catch { /* truncated */ }\n partials.delete(v.index as number)\n }\n break\n }\n case 'message_delta': {\n const d = v.delta as Record<string, unknown> | undefined\n if (d?.stop_reason) stopReason = d.stop_reason === 'tool_use' ? 'tool_use' : (d.stop_reason as string)\n const u = v.usage as Record<string, number> | undefined\n if (u?.output_tokens) outputTokens = u.output_tokens\n break\n }\n }\n return undefined\n },\n finish() {\n const usage = (inputTokens || outputTokens) ? { inputTokens, outputTokens } : undefined\n return { ...(toolCalls.length > 0 ? { toolCalls } : {}), stopReason, ...(usage ? { usage } : {}) }\n },\n }\n },\n}\n"],"mappings":"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","names":[]}
|