@cyrilmarin/dsh-lemonade 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.fr.md +186 -0
- package/README.md +189 -0
- package/lib/adapter.js +287 -0
- package/lib/client.js +561 -0
- package/lib/index.js +263 -0
- package/lib/serialize.js +113 -0
- package/lib/server-api.js +356 -0
- package/lib/translate.js +163 -0
- package/lib/types/adapter.d.ts +94 -0
- package/lib/types/index.d.ts +91 -0
- package/lib/types/serialize.d.ts +68 -0
- package/lib/types/server-api.d.ts +77 -0
- package/lib/types/translate.d.ts +10 -0
- package/package.json +94 -0
- package/src/adapter.ts +381 -0
- package/src/client/index.js +561 -0
- package/src/index.ts +323 -0
- package/src/serialize.ts +171 -0
- package/src/server-api.ts +402 -0
- package/src/translate.ts +190 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { INVALID_CREDENTIAL_CODE, LlmError, RetryPolicySchema, assertUsableApiKey, normalizeApiKey, resolveRetryPolicy, } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
3
|
+
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment';
|
|
4
|
+
import { deepEqualJson, installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
5
|
+
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout';
|
|
6
|
+
import z from '@deepseek-ai/schemastery';
|
|
7
|
+
import { DEFAULT_BASE_URL, DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_TOKENS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, LemonadeAdapter, discoverModels, } from './adapter.js';
|
|
8
|
+
import { API_ROUTE, createLemonadeApiHandler } from './server-api.js';
|
|
9
|
+
/** Short plugin name used in logs and configuration surfaces. */
|
|
10
|
+
export const name = 'llm-lemonade';
|
|
11
|
+
/** The provider route this plugin registers. */
|
|
12
|
+
export const PROVIDER = 'lemonade';
|
|
13
|
+
/** Hard service dependency: the LLM registry seat. */
|
|
14
|
+
export const inject = ['llm'];
|
|
15
|
+
/** Settings namespace this plugin owns. */
|
|
16
|
+
export const NS = settingsNamespace('llm-lemonade');
|
|
17
|
+
/** Environment variable naming this server's endpoint, honored from trusted layers only. */
|
|
18
|
+
export const BASE_URL_ENV = 'LEMONADE_BASE_URL';
|
|
19
|
+
/** Default environment variable naming the optional API key. */
|
|
20
|
+
export const DEFAULT_API_KEY_ENV = 'LEMONADE_API_KEY';
|
|
21
|
+
/** Default environment variable naming the optional admin API key (internal endpoints). */
|
|
22
|
+
export const DEFAULT_ADMIN_API_KEY_ENV = 'LEMONADE_ADMIN_API_KEY';
|
|
23
|
+
/** The static advisory model catalog schema (user-pinned entries). */
|
|
24
|
+
export const catalogModel = z.object({
|
|
25
|
+
id: z.string().required(),
|
|
26
|
+
name: z.string(),
|
|
27
|
+
description: z.string(),
|
|
28
|
+
contextWindow: z.number().step(1).min(1),
|
|
29
|
+
maxTokens: z.number().step(1).min(1),
|
|
30
|
+
vision: z.boolean(),
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Resolved plugin configuration schema. All fields except `baseURL` have
|
|
34
|
+
* defaults; `baseURL` itself can come from `LEMONADE_BASE_URL` when unset.
|
|
35
|
+
*/
|
|
36
|
+
export const Config = z.object({
|
|
37
|
+
baseURL: z.string(),
|
|
38
|
+
apiKeyEnv: z.string().role('credential-ref').default(DEFAULT_API_KEY_ENV),
|
|
39
|
+
adminApiKeyEnv: z.string().role('credential-ref').default(DEFAULT_ADMIN_API_KEY_ENV),
|
|
40
|
+
requireAuth: z.boolean().default(false),
|
|
41
|
+
defaultContextWindow: z.number().step(1).min(1).default(DEFAULT_CONTEXT_WINDOW),
|
|
42
|
+
maxTokens: z.number().step(1).min(1).default(DEFAULT_MAX_TOKENS),
|
|
43
|
+
models: z.array(catalogModel).default([]),
|
|
44
|
+
streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
|
45
|
+
retryPolicy: RetryPolicySchema,
|
|
46
|
+
});
|
|
47
|
+
/** Validate and detach the advisory model catalog. */
|
|
48
|
+
export function resolveModels(models) {
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
return (models ?? []).map((model) => {
|
|
51
|
+
if (model.id.length === 0)
|
|
52
|
+
throw new Error('llm-lemonade: catalog model ids must be non-empty');
|
|
53
|
+
if (model.name !== undefined && model.name.length === 0) {
|
|
54
|
+
throw new Error(`llm-lemonade: catalog model "${model.id}" has an empty name`);
|
|
55
|
+
}
|
|
56
|
+
if (model.contextWindow !== undefined && (!Number.isInteger(model.contextWindow) || model.contextWindow <= 0)) {
|
|
57
|
+
throw new Error(`llm-lemonade: catalog model "${model.id}" contextWindow must be a positive integer`);
|
|
58
|
+
}
|
|
59
|
+
if (model.maxTokens !== undefined && (!Number.isInteger(model.maxTokens) || model.maxTokens <= 0)) {
|
|
60
|
+
throw new Error(`llm-lemonade: catalog model "${model.id}" maxTokens must be a positive integer`);
|
|
61
|
+
}
|
|
62
|
+
if (seen.has(model.id))
|
|
63
|
+
throw new Error(`llm-lemonade: duplicate catalog model "${model.id}"`);
|
|
64
|
+
seen.add(model.id);
|
|
65
|
+
return {
|
|
66
|
+
id: model.id,
|
|
67
|
+
...(model.name !== undefined ? { name: model.name } : {}),
|
|
68
|
+
...(model.description !== undefined ? { description: model.description } : {}),
|
|
69
|
+
...(model.contextWindow !== undefined ? { contextWindow: model.contextWindow } : {}),
|
|
70
|
+
...(model.maxTokens !== undefined ? { maxTokens: model.maxTokens } : {}),
|
|
71
|
+
...(model.vision !== undefined ? { vision: model.vision } : {}),
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function normalizeBaseURL(raw) {
|
|
76
|
+
const trimmed = raw.trim();
|
|
77
|
+
if (trimmed.length === 0)
|
|
78
|
+
throw new Error('llm-lemonade: baseURL must not be empty');
|
|
79
|
+
let url;
|
|
80
|
+
try {
|
|
81
|
+
url = new URL(trimmed);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new Error(`llm-lemonade: baseURL cannot be parsed: "${trimmed}"`);
|
|
85
|
+
}
|
|
86
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
87
|
+
throw new Error(`llm-lemonade: baseURL must use http or https, got "${trimmed}"`);
|
|
88
|
+
}
|
|
89
|
+
// The path prefix is ALWAYS /api: any configured path (including a
|
|
90
|
+
// root base or a legacy trailing /v1) is normalized to scheme://host/api,
|
|
91
|
+
// and every endpoint builder appends exactly one /v1 on top.
|
|
92
|
+
return url.protocol + '//' + url.host + '/api';
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The explicit step from raw config to validated connection facts.
|
|
96
|
+
* Programmatic construction may bypass Schemastery normalization, so every
|
|
97
|
+
* default and bound is re-judged here — for the composition entry at load
|
|
98
|
+
* (fail loud) and for each settings snapshot at its first use.
|
|
99
|
+
*
|
|
100
|
+
* @param config - raw plugin config or resolved settings snapshot.
|
|
101
|
+
* @param environment - this run's environment layers, when the product CLI provided them.
|
|
102
|
+
* @returns validated connection facts plus the credential reference.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveAdapterOptions(config, environment) {
|
|
105
|
+
if (config.defaultContextWindow !== undefined && (!Number.isInteger(config.defaultContextWindow) || config.defaultContextWindow <= 0)) {
|
|
106
|
+
throw new Error('llm-lemonade: defaultContextWindow must be a positive integer');
|
|
107
|
+
}
|
|
108
|
+
if (config.maxTokens !== undefined && (!Number.isSafeInteger(config.maxTokens) || config.maxTokens <= 0)) {
|
|
109
|
+
throw new Error('llm-lemonade: maxTokens must be a positive safe integer');
|
|
110
|
+
}
|
|
111
|
+
const streamIdleTimeoutMs = config.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
|
|
112
|
+
if (!Number.isFinite(streamIdleTimeoutMs) || streamIdleTimeoutMs <= 0 || streamIdleTimeoutMs > MAX_TIMER_DELAY_MS) {
|
|
113
|
+
throw new Error(`llm-lemonade: streamIdleTimeoutMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
114
|
+
}
|
|
115
|
+
const rawBase = config.baseURL ?? environment?.get(BASE_URL_ENV)?.value ?? DEFAULT_BASE_URL;
|
|
116
|
+
return {
|
|
117
|
+
apiKeyEnv: credentialRef(config.apiKeyEnv ?? DEFAULT_API_KEY_ENV),
|
|
118
|
+
adminApiKeyEnv: credentialRef(config.adminApiKeyEnv ?? DEFAULT_ADMIN_API_KEY_ENV),
|
|
119
|
+
baseURL: normalizeBaseURL(rawBase),
|
|
120
|
+
requireAuth: config.requireAuth === true,
|
|
121
|
+
defaultContextWindow: config.defaultContextWindow ?? DEFAULT_CONTEXT_WINDOW,
|
|
122
|
+
maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
123
|
+
models: resolveModels(config.models),
|
|
124
|
+
streamIdleTimeoutMs,
|
|
125
|
+
retryPolicy: resolveRetryPolicy(config.retryPolicy, 'llm-lemonade: retryPolicy'),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** Resolve the optional bearer token, enforcing `requireAuth` when configured. */
|
|
129
|
+
function makeResolveApiKey(ctx, options) {
|
|
130
|
+
return async () => {
|
|
131
|
+
const connection = options();
|
|
132
|
+
const ref = connection.apiKeyEnv;
|
|
133
|
+
let value;
|
|
134
|
+
const credentials = ctx.get('credentials');
|
|
135
|
+
if (credentials !== undefined)
|
|
136
|
+
value = (await credentials.resolve(ref))?.value;
|
|
137
|
+
if (value === undefined)
|
|
138
|
+
value = launchEnvironmentOf(ctx).get(ref)?.value;
|
|
139
|
+
if (value === undefined || value.length === 0) {
|
|
140
|
+
if (connection.requireAuth) {
|
|
141
|
+
throw new LlmError(`llm-lemonade: no API key for provider route "${PROVIDER}"; store ${String(ref)} through the credentials service (the web Models page writes it), or export ${String(ref)} in the launching environment`, 'MISSING_CREDENTIAL');
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
return assertUsableApiKey(value, 'llm-lemonade', String(ref));
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Register a {@link LemonadeAdapter} for the `lemonade` provider route.
|
|
150
|
+
* See the module header for the layering story.
|
|
151
|
+
*/
|
|
152
|
+
export function apply(ctx, config) {
|
|
153
|
+
let current = () => config;
|
|
154
|
+
let lastRaw;
|
|
155
|
+
let lastGood;
|
|
156
|
+
const options = () => {
|
|
157
|
+
const raw = current();
|
|
158
|
+
if (raw === lastRaw && lastGood !== undefined)
|
|
159
|
+
return lastGood;
|
|
160
|
+
try {
|
|
161
|
+
const next = resolveAdapterOptions(raw, launchEnvironmentOf(ctx));
|
|
162
|
+
lastRaw = raw;
|
|
163
|
+
lastGood = next;
|
|
164
|
+
return next;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (lastGood === undefined)
|
|
168
|
+
throw error;
|
|
169
|
+
lastRaw = raw;
|
|
170
|
+
ctx.logger.error('llm-lemonade: keeping the last good configuration after an invalid settings section');
|
|
171
|
+
ctx.logger.error(error);
|
|
172
|
+
return lastGood;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
options();
|
|
176
|
+
const resolveApiKey = makeResolveApiKey(ctx, options);
|
|
177
|
+
const adapter = new LemonadeAdapter({
|
|
178
|
+
options,
|
|
179
|
+
resolveApiKey,
|
|
180
|
+
resolveAttachments: () => ctx.get('attachments'),
|
|
181
|
+
});
|
|
182
|
+
ctx.llm.registerConfigurableProviders([
|
|
183
|
+
{ provider: PROVIDER, displayName: 'Lemonade', settingsNs: NS, settingsPath: [] },
|
|
184
|
+
]);
|
|
185
|
+
const registration = ctx.llm.registerAdapter([PROVIDER], adapter);
|
|
186
|
+
let registeredPolicy = options().retryPolicy;
|
|
187
|
+
const ensureRegistrationFacts = () => {
|
|
188
|
+
const policy = options().retryPolicy;
|
|
189
|
+
if (deepEqualJson(policy, registeredPolicy))
|
|
190
|
+
return;
|
|
191
|
+
registration.replace([PROVIDER]);
|
|
192
|
+
registeredPolicy = policy;
|
|
193
|
+
};
|
|
194
|
+
ctx.llm.registerModelDiscovery(NS, async (request) => {
|
|
195
|
+
const baseURL = request.baseURL !== undefined && request.baseURL.length > 0 ? normalizeBaseURL(request.baseURL) : options().baseURL;
|
|
196
|
+
let apiKey;
|
|
197
|
+
if (request.apiKey !== undefined) {
|
|
198
|
+
const checked = normalizeApiKey(request.apiKey);
|
|
199
|
+
if (!checked.ok) {
|
|
200
|
+
throw new LlmError(checked.reason === 'empty'
|
|
201
|
+
? 'this server\'s API key is blank; enter it on the Models page, or clear it to probe unauthenticated'
|
|
202
|
+
: 'this server\'s API key contains characters no HTTP header can carry; paste the raw key only', INVALID_CREDENTIAL_CODE);
|
|
203
|
+
}
|
|
204
|
+
apiKey = checked.value;
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
apiKey = await resolveApiKey();
|
|
208
|
+
}
|
|
209
|
+
return discoverModels(baseURL, apiKey, request.signal);
|
|
210
|
+
});
|
|
211
|
+
// Lemonade-specific API proxy: browser client half calls these routes
|
|
212
|
+
// same-origin; the keys are resolved host-side and never reach the browser.
|
|
213
|
+
// Per-endpoint key selection (regular vs admin) lives in server-api.ts.
|
|
214
|
+
const resolveKey = async (ref) => {
|
|
215
|
+
let value;
|
|
216
|
+
const credentials = ctx.get('credentials');
|
|
217
|
+
if (credentials !== undefined)
|
|
218
|
+
value = (await credentials.resolve(ref))?.value;
|
|
219
|
+
if (value === undefined)
|
|
220
|
+
value = launchEnvironmentOf(ctx).get(ref)?.value;
|
|
221
|
+
if (value === undefined || value.length === 0)
|
|
222
|
+
return undefined;
|
|
223
|
+
return assertUsableApiKey(value, 'llm-lemonade', String(ref));
|
|
224
|
+
};
|
|
225
|
+
const apiCfg = {
|
|
226
|
+
baseURL: () => options().baseURL,
|
|
227
|
+
requireAuth: () => options().requireAuth,
|
|
228
|
+
apiKeyRef: () => options().apiKeyEnv,
|
|
229
|
+
adminApiKeyRef: () => options().adminApiKeyEnv,
|
|
230
|
+
resolveKey,
|
|
231
|
+
};
|
|
232
|
+
// Register the proxy route once the webServer service is available. It is
|
|
233
|
+
// mounted late (after the llm service this plugin depends on), so a strict
|
|
234
|
+
// ctx.get() at apply time is usually undefined; instead grab it eagerly
|
|
235
|
+
// (non-strict) and fall back to binding the route when 'webServer' is
|
|
236
|
+
// provided. Absent webServer (headless profile), nothing is registered and
|
|
237
|
+
// the host half keeps working as a plain model provider.
|
|
238
|
+
let routesRegistered = false;
|
|
239
|
+
const registerRoutes = () => {
|
|
240
|
+
if (routesRegistered)
|
|
241
|
+
return;
|
|
242
|
+
const server = ctx.get('webServer', false);
|
|
243
|
+
if (server === undefined)
|
|
244
|
+
return;
|
|
245
|
+
routesRegistered = true;
|
|
246
|
+
ctx.effect(() => server.register({ kind: 'prefix', path: API_ROUTE, handler: createLemonadeApiHandler(apiCfg) }));
|
|
247
|
+
};
|
|
248
|
+
registerRoutes();
|
|
249
|
+
if (!routesRegistered) {
|
|
250
|
+
ctx.on('internal/service', (name) => {
|
|
251
|
+
if (name === 'webServer')
|
|
252
|
+
registerRoutes();
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
installSettingsSection(ctx, NS, Config, config, {
|
|
256
|
+
setSource: (source) => {
|
|
257
|
+
current = source;
|
|
258
|
+
},
|
|
259
|
+
onChange: ensureRegistrationFacts,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
export { LemonadeAdapter };
|
|
263
|
+
//# sourceMappingURL=index.js.map
|
package/lib/serialize.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { contentHasImage, LlmError } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
/** Join the text blocks of a message (user and tool-result content). */
|
|
3
|
+
function flattenText(blocks) {
|
|
4
|
+
return blocks
|
|
5
|
+
.filter((block) => block.type === 'text')
|
|
6
|
+
.map((block) => block.text)
|
|
7
|
+
.join('');
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Serialize a user message's content. Text-only messages stay a plain string;
|
|
11
|
+
* a message carrying one or more image blocks becomes an array of parts that
|
|
12
|
+
* preserves block order.
|
|
13
|
+
*/
|
|
14
|
+
async function serializeUserContent(blocks, resolveImage, signal) {
|
|
15
|
+
const parts = [];
|
|
16
|
+
let hasImage = false;
|
|
17
|
+
for (const block of blocks) {
|
|
18
|
+
if (block.type === 'text') {
|
|
19
|
+
parts.push({ type: 'text', text: block.text });
|
|
20
|
+
}
|
|
21
|
+
else if (block.type === 'image') {
|
|
22
|
+
hasImage = true;
|
|
23
|
+
parts.push({ type: 'image_url', image_url: { url: await resolveImage(block.attachment, signal) } });
|
|
24
|
+
}
|
|
25
|
+
// tool-result blocks are expanded into standalone tool messages by the caller.
|
|
26
|
+
}
|
|
27
|
+
if (!hasImage)
|
|
28
|
+
return parts.map((part) => (part.type === 'text' ? part.text : '')).join('');
|
|
29
|
+
return parts;
|
|
30
|
+
}
|
|
31
|
+
/** Serialize one assistant message (text + tool calls; reasoning dropped). */
|
|
32
|
+
function serializeAssistant(message) {
|
|
33
|
+
const text = flattenText(message.content);
|
|
34
|
+
const toolCalls = message.content
|
|
35
|
+
.filter((block) => block.type === 'tool-call')
|
|
36
|
+
.map((block) => ({
|
|
37
|
+
id: block.id,
|
|
38
|
+
type: 'function',
|
|
39
|
+
function: { name: block.name, arguments: block.arguments },
|
|
40
|
+
}));
|
|
41
|
+
if (toolCalls.length === 0)
|
|
42
|
+
return { role: 'assistant', content: text };
|
|
43
|
+
// OpenAI canonical form: content is null when the turn is tool calls only.
|
|
44
|
+
return { role: 'assistant', content: text.length > 0 ? text : null, tool_calls: toolCalls };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Serialize the conversation in order. A mixed user message contributes its
|
|
48
|
+
* text/image content first and each tool result as a standalone wire message
|
|
49
|
+
* after it.
|
|
50
|
+
*
|
|
51
|
+
* @param messages - the harness conversation, in order.
|
|
52
|
+
* @param resolveImage - resolves image blocks; required whenever one is present.
|
|
53
|
+
* @param signal - cancellation forwarded to attachment reads.
|
|
54
|
+
*/
|
|
55
|
+
async function serializeMessages(messages, resolveImage, signal) {
|
|
56
|
+
const wire = [];
|
|
57
|
+
for (const message of messages) {
|
|
58
|
+
if (message.role === 'system') {
|
|
59
|
+
wire.push({ role: 'system', content: flattenText(message.content) });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (message.role === 'assistant') {
|
|
63
|
+
wire.push(serializeAssistant(message));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const toolResults = message.content.filter((block) => block.type === 'tool-result');
|
|
67
|
+
const content = await serializeUserContent(message.content, resolveImage, signal);
|
|
68
|
+
const hasText = typeof content === 'string' ? content.length > 0 : true;
|
|
69
|
+
if (hasText || toolResults.length === 0)
|
|
70
|
+
wire.push({ role: 'user', content });
|
|
71
|
+
for (const result of toolResults) {
|
|
72
|
+
wire.push({ role: 'tool', tool_call_id: result.toolCallId, content: flattenText(result.content) || '(no output)' });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return wire;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Build the full wire request body. Always streaming with usage reporting on
|
|
79
|
+
* (Lemonade's llamacpp backends honor `stream_options.include_usage`; servers
|
|
80
|
+
* that ignore it simply never send a usage chunk, and the translate step
|
|
81
|
+
* tolerates that). Optional sampling fields are omitted rather than sent as
|
|
82
|
+
* null so provider defaults apply.
|
|
83
|
+
*
|
|
84
|
+
* @param options - the harness request (model, history, system, tools, sampling).
|
|
85
|
+
* @param resolveImage - resolves image blocks; `undefined` when no attachment
|
|
86
|
+
* service is available, in which case image content is refused up front.
|
|
87
|
+
*/
|
|
88
|
+
export async function serializeRequest(options, resolveImage) {
|
|
89
|
+
if (resolveImage === undefined && options.messages.some((message) => contentHasImage(message.content))) {
|
|
90
|
+
throw new LlmError('The Lemonade adapter requires the attachment service to send image content.', 'UNSUPPORTED_CONTENT');
|
|
91
|
+
}
|
|
92
|
+
const messages = [];
|
|
93
|
+
if (options.system !== undefined)
|
|
94
|
+
messages.push({ role: 'system', content: options.system });
|
|
95
|
+
messages.push(...(await serializeMessages(options.messages, resolveImage ?? (() => { throw new LlmError('no image resolver', 'UNSUPPORTED_CONTENT'); }), options.signal)));
|
|
96
|
+
const tools = options.tools?.length
|
|
97
|
+
? options.tools.map((tool) => ({
|
|
98
|
+
type: 'function',
|
|
99
|
+
function: { name: tool.name, description: tool.description, parameters: tool.parameters },
|
|
100
|
+
}))
|
|
101
|
+
: undefined;
|
|
102
|
+
return {
|
|
103
|
+
model: options.model,
|
|
104
|
+
messages,
|
|
105
|
+
stream: true,
|
|
106
|
+
stream_options: { include_usage: true },
|
|
107
|
+
...(tools !== undefined ? { tools } : {}),
|
|
108
|
+
...(options.temperature !== undefined ? { temperature: options.temperature } : {}),
|
|
109
|
+
...(options.maxTokens === undefined ? {} : { max_tokens: options.maxTokens }),
|
|
110
|
+
...(options.stop !== undefined ? { stop: options.stop } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=serialize.js.map
|