@startsimpli/llm 0.4.22 → 0.4.23
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/package.json
CHANGED
|
@@ -169,3 +169,14 @@ describe('AnthropicProvider — recovering from a parameter the model rejects',
|
|
|
169
169
|
expect(sent).toHaveLength(1);
|
|
170
170
|
});
|
|
171
171
|
});
|
|
172
|
+
|
|
173
|
+
describe('images', () => {
|
|
174
|
+
it('sends images as base64 image blocks before the text', async () => {
|
|
175
|
+
await provider('claude-opus-5-5').generate('what is this?', 's', { images: [{ data: 'AAAA', mediaType: 'image/png' }] });
|
|
176
|
+
const body = sent[0] as unknown as { messages: Array<{ content: Array<Record<string, unknown>> }> };
|
|
177
|
+
const content = body.messages[0]!.content;
|
|
178
|
+
expect(content[0]).toEqual({ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'AAAA' } });
|
|
179
|
+
expect(content[1]!.type).toBe('text');
|
|
180
|
+
expect(String(content[1]!.text)).toContain('what is this?');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -265,3 +265,18 @@ describe('generateStream', () => {
|
|
|
265
265
|
await expect(provider('gpt-6-astra').generateStream!('u', 's', () => {})).rejects.toThrow(/Empty response/);
|
|
266
266
|
});
|
|
267
267
|
});
|
|
268
|
+
|
|
269
|
+
describe('images', () => {
|
|
270
|
+
it('sends images as image_url parts after the text, and plain text without them', async () => {
|
|
271
|
+
const p = provider('gpt-6-astra');
|
|
272
|
+
await p.generate('what is this?', 's', { images: [{ data: 'AAAA', mediaType: 'image/png' }] });
|
|
273
|
+
await p.generate('plain', 's');
|
|
274
|
+
const withImage = sent[0] as unknown as { messages: Array<{ role: string; content: unknown }> };
|
|
275
|
+
expect(withImage.messages[1]!.content).toEqual([
|
|
276
|
+
{ type: 'text', text: 'what is this?' },
|
|
277
|
+
{ type: 'image_url', image_url: { url: 'data:image/png;base64,AAAA' } },
|
|
278
|
+
]);
|
|
279
|
+
const plain = sent[1] as unknown as { messages: Array<{ role: string; content: unknown }> };
|
|
280
|
+
expect(plain.messages[1]!.content).toBe('plain');
|
|
281
|
+
});
|
|
282
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -11,9 +11,13 @@ import { LLM_DEFAULTS } from '../types';
|
|
|
11
11
|
import { LLMProviderError } from '../errors';
|
|
12
12
|
import type { LLMErrorCode } from '../errors';
|
|
13
13
|
|
|
14
|
+
type AnthropicContentBlock =
|
|
15
|
+
| { type: 'text'; text: string }
|
|
16
|
+
| { type: 'image'; source: { type: 'base64'; media_type: string; data: string } };
|
|
17
|
+
|
|
14
18
|
interface AnthropicMessage {
|
|
15
19
|
role: 'user' | 'assistant';
|
|
16
|
-
content: string;
|
|
20
|
+
content: string | AnthropicContentBlock[];
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
interface AnthropicRequest {
|
|
@@ -199,7 +203,18 @@ Please respond with a valid JSON object only. Do not include any text before or
|
|
|
199
203
|
const requestBody: AnthropicRequest = {
|
|
200
204
|
model,
|
|
201
205
|
max_tokens: maxTokens,
|
|
202
|
-
messages: [{
|
|
206
|
+
messages: [{
|
|
207
|
+
role: 'user',
|
|
208
|
+
content: options?.images?.length
|
|
209
|
+
? [
|
|
210
|
+
...options.images.map((img) => ({
|
|
211
|
+
type: 'image' as const,
|
|
212
|
+
source: { type: 'base64' as const, media_type: img.mediaType, data: img.data },
|
|
213
|
+
})),
|
|
214
|
+
{ type: 'text' as const, text: jsonWrappedPrompt },
|
|
215
|
+
]
|
|
216
|
+
: jsonWrappedPrompt,
|
|
217
|
+
}],
|
|
203
218
|
system: systemPrompt,
|
|
204
219
|
};
|
|
205
220
|
// Only for models known to take it. The Claude 5 family answers a request
|
package/src/providers/openai.ts
CHANGED
|
@@ -18,14 +18,28 @@ import type {
|
|
|
18
18
|
NativeChatMessage,
|
|
19
19
|
NativeToolResponse,
|
|
20
20
|
ReasoningEffort,
|
|
21
|
+
LLMImage,
|
|
21
22
|
} from '../types';
|
|
22
23
|
import { LLM_DEFAULTS } from '../types';
|
|
23
24
|
import { LLMProviderError } from '../errors';
|
|
24
25
|
import type { LLMErrorCode } from '../errors';
|
|
25
26
|
|
|
27
|
+
type OpenAIContentPart =
|
|
28
|
+
| { type: 'text'; text: string }
|
|
29
|
+
| { type: 'image_url'; image_url: { url: string } };
|
|
30
|
+
|
|
26
31
|
interface OpenAIMessage {
|
|
27
32
|
role: 'system' | 'user' | 'assistant';
|
|
28
|
-
content: string;
|
|
33
|
+
content: string | OpenAIContentPart[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The user message: plain text, or text plus images for vision models. */
|
|
37
|
+
function userContent(prompt: string, images?: LLMImage[]): string | OpenAIContentPart[] {
|
|
38
|
+
if (!images?.length) return prompt;
|
|
39
|
+
return [
|
|
40
|
+
{ type: 'text', text: prompt },
|
|
41
|
+
...images.map((img) => ({ type: 'image_url' as const, image_url: { url: `data:${img.mediaType};base64,${img.data}` } })),
|
|
42
|
+
];
|
|
29
43
|
}
|
|
30
44
|
|
|
31
45
|
interface OpenAIRequest {
|
|
@@ -324,7 +338,7 @@ export class OpenAIProvider implements ILLMProvider {
|
|
|
324
338
|
model,
|
|
325
339
|
messages: [
|
|
326
340
|
{ role: 'system' as const, content: systemPrompt },
|
|
327
|
-
{ role: 'user' as const, content: userPrompt },
|
|
341
|
+
{ role: 'user' as const, content: userContent(userPrompt, options?.images) },
|
|
328
342
|
],
|
|
329
343
|
response_format: { type: 'json_object' as const },
|
|
330
344
|
stream: true,
|
|
@@ -424,7 +438,7 @@ export class OpenAIProvider implements ILLMProvider {
|
|
|
424
438
|
model,
|
|
425
439
|
messages: [
|
|
426
440
|
{ role: 'system' as const, content: systemPrompt },
|
|
427
|
-
{ role: 'user' as const, content: userPrompt },
|
|
441
|
+
{ role: 'user' as const, content: userContent(userPrompt, options?.images) },
|
|
428
442
|
],
|
|
429
443
|
response_format: { type: 'json_object' as const },
|
|
430
444
|
};
|
package/src/types.ts
CHANGED
|
@@ -30,6 +30,12 @@ export interface LLMResponse {
|
|
|
30
30
|
/**
|
|
31
31
|
* Options passed through to the provider for a single generation call.
|
|
32
32
|
*/
|
|
33
|
+
/** An image attached to a prompt: base64 data (no data: prefix) and its media type. */
|
|
34
|
+
export interface LLMImage {
|
|
35
|
+
data: string;
|
|
36
|
+
mediaType: 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp';
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
export interface GenerateOptions {
|
|
34
40
|
/** Model to use (provider-specific) */
|
|
35
41
|
model?: string;
|
|
@@ -45,6 +51,11 @@ export interface GenerateOptions {
|
|
|
45
51
|
* provider adds headroom for the reasoning on top.
|
|
46
52
|
*/
|
|
47
53
|
reasoningEffort?: ReasoningEffort;
|
|
54
|
+
/**
|
|
55
|
+
* Images sent with the user prompt, for vision-capable models (e.g. to have
|
|
56
|
+
* a model look at a rendering of what it designed).
|
|
57
|
+
*/
|
|
58
|
+
images?: LLMImage[];
|
|
48
59
|
/** Arbitrary extra data the caller wants to thread through (e.g. domain preferences) */
|
|
49
60
|
extra?: Record<string, unknown>;
|
|
50
61
|
}
|