Skip to content

Commit aaee677

Browse files
committed
fork openai-compatible v5
1 parent fbc9652 commit aaee677

28 files changed

+6947
-0
lines changed

packages/internal/src/openai-compatible/chat/convert-to-openai-compatible-chat-messages.test.ts

Lines changed: 690 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import {
2+
LanguageModelV2Prompt,
3+
SharedV2ProviderMetadata,
4+
UnsupportedFunctionalityError,
5+
} from '@ai-sdk/provider';
6+
import { OpenAICompatibleChatPrompt } from './openai-compatible-api-types';
7+
import { convertToBase64 } from '@ai-sdk/provider-utils';
8+
9+
function getOpenAIMetadata(message: {
10+
providerOptions?: SharedV2ProviderMetadata;
11+
}) {
12+
return message?.providerOptions?.openaiCompatible ?? {};
13+
}
14+
15+
export function convertToOpenAICompatibleChatMessages(
16+
prompt: LanguageModelV2Prompt,
17+
): OpenAICompatibleChatPrompt {
18+
const messages: OpenAICompatibleChatPrompt = [];
19+
for (const { role, content, ...message } of prompt) {
20+
const metadata = getOpenAIMetadata({ ...message });
21+
switch (role) {
22+
case 'system': {
23+
messages.push({ role: 'system', content, ...metadata });
24+
break;
25+
}
26+
27+
case 'user': {
28+
if (content.length === 1 && content[0].type === 'text') {
29+
messages.push({
30+
role: 'user',
31+
content: content[0].text,
32+
...getOpenAIMetadata(content[0]),
33+
});
34+
break;
35+
}
36+
37+
messages.push({
38+
role: 'user',
39+
content: content.map(part => {
40+
const partMetadata = getOpenAIMetadata(part);
41+
switch (part.type) {
42+
case 'text': {
43+
return { type: 'text', text: part.text, ...partMetadata };
44+
}
45+
case 'file': {
46+
if (part.mediaType.startsWith('image/')) {
47+
const mediaType =
48+
part.mediaType === 'image/*'
49+
? 'image/jpeg'
50+
: part.mediaType;
51+
52+
return {
53+
type: 'image_url',
54+
image_url: {
55+
url:
56+
part.data instanceof URL
57+
? part.data.toString()
58+
: `data:${mediaType};base64,${convertToBase64(part.data)}`,
59+
},
60+
...partMetadata,
61+
};
62+
} else {
63+
throw new UnsupportedFunctionalityError({
64+
functionality: `file part media type ${part.mediaType}`,
65+
});
66+
}
67+
}
68+
}
69+
}),
70+
...metadata,
71+
});
72+
73+
break;
74+
}
75+
76+
case 'assistant': {
77+
let text = '';
78+
const toolCalls: Array<{
79+
id: string;
80+
type: 'function';
81+
function: { name: string; arguments: string };
82+
}> = [];
83+
84+
for (const part of content) {
85+
const partMetadata = getOpenAIMetadata(part);
86+
switch (part.type) {
87+
case 'text': {
88+
text += part.text;
89+
break;
90+
}
91+
case 'tool-call': {
92+
toolCalls.push({
93+
id: part.toolCallId,
94+
type: 'function',
95+
function: {
96+
name: part.toolName,
97+
arguments: JSON.stringify(part.input),
98+
},
99+
...partMetadata,
100+
});
101+
break;
102+
}
103+
}
104+
}
105+
106+
messages.push({
107+
role: 'assistant',
108+
content: text,
109+
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
110+
...metadata,
111+
});
112+
113+
break;
114+
}
115+
116+
case 'tool': {
117+
for (const toolResponse of content) {
118+
const output = toolResponse.output;
119+
120+
let contentValue: string;
121+
switch (output.type) {
122+
case 'text':
123+
case 'error-text':
124+
contentValue = output.value;
125+
break;
126+
case 'content':
127+
case 'json':
128+
case 'error-json':
129+
contentValue = JSON.stringify(output.value);
130+
break;
131+
}
132+
133+
const toolResponseMetadata = getOpenAIMetadata(toolResponse);
134+
messages.push({
135+
role: 'tool',
136+
tool_call_id: toolResponse.toolCallId,
137+
content: contentValue,
138+
...toolResponseMetadata,
139+
});
140+
}
141+
break;
142+
}
143+
144+
default: {
145+
const _exhaustiveCheck: never = role;
146+
throw new Error(`Unsupported role: ${_exhaustiveCheck}`);
147+
}
148+
}
149+
}
150+
151+
return messages;
152+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export function getResponseMetadata({
2+
id,
3+
model,
4+
created,
5+
}: {
6+
id?: string | undefined | null;
7+
created?: number | undefined | null;
8+
model?: string | undefined | null;
9+
}) {
10+
return {
11+
id: id ?? undefined,
12+
modelId: model ?? undefined,
13+
timestamp: created != null ? new Date(created * 1000) : undefined,
14+
};
15+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { LanguageModelV2FinishReason } from '@ai-sdk/provider';
2+
3+
export function mapOpenAICompatibleFinishReason(
4+
finishReason: string | null | undefined,
5+
): LanguageModelV2FinishReason {
6+
switch (finishReason) {
7+
case 'stop':
8+
return 'stop';
9+
case 'length':
10+
return 'length';
11+
case 'content_filter':
12+
return 'content-filter';
13+
case 'function_call':
14+
case 'tool_calls':
15+
return 'tool-calls';
16+
default:
17+
return 'unknown';
18+
}
19+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { JSONValue } from '@ai-sdk/provider';
2+
3+
export type OpenAICompatibleChatPrompt = Array<OpenAICompatibleMessage>;
4+
5+
export type OpenAICompatibleMessage =
6+
| OpenAICompatibleSystemMessage
7+
| OpenAICompatibleUserMessage
8+
| OpenAICompatibleAssistantMessage
9+
| OpenAICompatibleToolMessage;
10+
11+
// Allow for arbitrary additional properties for general purpose
12+
// provider-metadata-specific extensibility.
13+
type JsonRecord<T = never> = Record<
14+
string,
15+
JSONValue | JSONValue[] | T | T[] | undefined
16+
>;
17+
18+
export interface OpenAICompatibleSystemMessage extends JsonRecord {
19+
role: 'system';
20+
content: string;
21+
}
22+
23+
export interface OpenAICompatibleUserMessage
24+
extends JsonRecord<OpenAICompatibleContentPart> {
25+
role: 'user';
26+
content: string | Array<OpenAICompatibleContentPart>;
27+
}
28+
29+
export type OpenAICompatibleContentPart =
30+
| OpenAICompatibleContentPartText
31+
| OpenAICompatibleContentPartImage;
32+
33+
export interface OpenAICompatibleContentPartImage extends JsonRecord {
34+
type: 'image_url';
35+
image_url: { url: string };
36+
}
37+
38+
export interface OpenAICompatibleContentPartText extends JsonRecord {
39+
type: 'text';
40+
text: string;
41+
}
42+
43+
export interface OpenAICompatibleAssistantMessage
44+
extends JsonRecord<OpenAICompatibleMessageToolCall> {
45+
role: 'assistant';
46+
content?: string | null;
47+
tool_calls?: Array<OpenAICompatibleMessageToolCall>;
48+
}
49+
50+
export interface OpenAICompatibleMessageToolCall extends JsonRecord {
51+
type: 'function';
52+
id: string;
53+
function: {
54+
arguments: string;
55+
name: string;
56+
};
57+
}
58+
59+
export interface OpenAICompatibleToolMessage extends JsonRecord {
60+
role: 'tool';
61+
content: string;
62+
tool_call_id: string;
63+
}

0 commit comments

Comments
 (0)