mirror of
https://github.com/ONLYOFFICE/desktop-sdk.git
synced 2026-03-31 10:23:12 +08:00
Merge branch 'release/v9.3.0' into feature/ai-agent-langs_9.3
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -17,6 +17,7 @@
|
|||||||
"@codemirror/view": "^6.38.2",
|
"@codemirror/view": "^6.38.2",
|
||||||
"@google/genai": "^1.30.0",
|
"@google/genai": "^1.30.0",
|
||||||
"@lmstudio/sdk": "^1.5.0",
|
"@lmstudio/sdk": "^1.5.0",
|
||||||
|
"@mistralai/mistralai": "^1.11.0",
|
||||||
"@openrouter/ai-sdk-provider": "^1.2.3",
|
"@openrouter/ai-sdk-provider": "^1.2.3",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
@ -1420,6 +1421,15 @@
|
|||||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@mistralai/mistralai": {
|
||||||
|
"version": "1.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.11.0.tgz",
|
||||||
|
"integrity": "sha512-6/BVj2mcaggYbpMzNSxtqtM2Tv/Jb5845XFd2CMYFO+O5VBkX70iLjtkBBTI4JFhh1l9vTCIMYXBVOjLoBVHGQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.20.0",
|
||||||
|
"zod-to-json-schema": "^3.24.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@openrouter/ai-sdk-provider": {
|
"node_modules/@openrouter/ai-sdk-provider": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-1.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-1.5.4.tgz",
|
||||||
|
|||||||
@ -30,6 +30,7 @@
|
|||||||
"@codemirror/view": "^6.38.2",
|
"@codemirror/view": "^6.38.2",
|
||||||
"@google/genai": "^1.30.0",
|
"@google/genai": "^1.30.0",
|
||||||
"@lmstudio/sdk": "^1.5.0",
|
"@lmstudio/sdk": "^1.5.0",
|
||||||
|
"@mistralai/mistralai": "^1.11.0",
|
||||||
"@openrouter/ai-sdk-provider": "^1.2.3",
|
"@openrouter/ai-sdk-provider": "^1.2.3",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
|||||||
@ -22,7 +22,8 @@ export type ProviderType =
|
|||||||
| "genai"
|
| "genai"
|
||||||
| "deepseek"
|
| "deepseek"
|
||||||
| "xai"
|
| "xai"
|
||||||
| "lm-studio";
|
| "lm-studio"
|
||||||
|
| "mistral";
|
||||||
|
|
||||||
export type Model = {
|
export type Model = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@ -24,8 +24,6 @@ const AvailableTools = () => {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setAllToolsCount(webSearchEnabled ? tools.length - 2 : tools.length);
|
setAllToolsCount(webSearchEnabled ? tools.length - 2 : tools.length);
|
||||||
|
|
||||||
console.log(tools.length);
|
|
||||||
}, [tools.length, webSearchEnabled]);
|
}, [tools.length, webSearchEnabled]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
import type { ThreadMessageLike } from "@assistant-ui/react";
|
||||||
|
import type {
|
||||||
|
CompletionEvent,
|
||||||
|
DeltaMessage,
|
||||||
|
} from "@mistralai/mistralai/models/components";
|
||||||
|
|
||||||
|
export const handleToolCall = (
|
||||||
|
delta: DeltaMessage,
|
||||||
|
responseMessage: ThreadMessageLike
|
||||||
|
): ThreadMessageLike => {
|
||||||
|
if (!delta.toolCalls?.length) return responseMessage;
|
||||||
|
|
||||||
|
const toolCall = delta.toolCalls[0];
|
||||||
|
const toolCallId = toolCall.id || "";
|
||||||
|
const toolCallArgs = toolCall.function?.arguments || "{}";
|
||||||
|
const toolCallContent: ThreadMessageLike["content"] = [
|
||||||
|
{
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId,
|
||||||
|
toolName: toolCall.function?.name || "",
|
||||||
|
args:
|
||||||
|
typeof toolCallArgs === "string"
|
||||||
|
? JSON.parse(toolCallArgs)
|
||||||
|
: toolCallArgs,
|
||||||
|
argsText:
|
||||||
|
typeof toolCallArgs === "string"
|
||||||
|
? toolCallArgs
|
||||||
|
: JSON.stringify(toolCallArgs),
|
||||||
|
result: "",
|
||||||
|
parentId: toolCallId,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (typeof responseMessage.content !== "string") {
|
||||||
|
return {
|
||||||
|
...responseMessage,
|
||||||
|
content: [...responseMessage.content, ...toolCallContent],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleTextContent = (
|
||||||
|
delta: DeltaMessage,
|
||||||
|
responseMessage: ThreadMessageLike
|
||||||
|
): ThreadMessageLike => {
|
||||||
|
if (
|
||||||
|
typeof delta?.content !== "string" ||
|
||||||
|
typeof responseMessage.content === "string"
|
||||||
|
) {
|
||||||
|
return responseMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = delta.content;
|
||||||
|
const lastContent = responseMessage.content.at(-1);
|
||||||
|
|
||||||
|
if (lastContent && lastContent.type === "text") {
|
||||||
|
return {
|
||||||
|
...responseMessage,
|
||||||
|
content: [
|
||||||
|
...responseMessage.content.slice(0, -1),
|
||||||
|
{ ...lastContent, text: lastContent.text + content },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...responseMessage,
|
||||||
|
content: [...responseMessage.content, { type: "text", text: content }],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getChoiceFromEvent = (
|
||||||
|
event: CompletionEvent
|
||||||
|
): CompletionEvent["data"]["choices"][number] | undefined => {
|
||||||
|
return event.data?.choices?.[0];
|
||||||
|
};
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import type { ThreadMessageLike } from "@assistant-ui/react";
|
||||||
|
import { Mistral } from "@mistralai/mistralai";
|
||||||
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
import type { ToolCallPart } from "./types";
|
||||||
|
|
||||||
|
export const createClient = (apiKey?: string, baseUrl?: string): Mistral => {
|
||||||
|
return new Mistral({
|
||||||
|
apiKey: apiKey ?? "",
|
||||||
|
serverURL: baseUrl,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createEmptyResponse = (): ThreadMessageLike => ({
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createErrorResponse = (error: unknown): ThreadMessageLike => ({
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createResponseShell = (
|
||||||
|
afterToolCall?: boolean,
|
||||||
|
existingMessage?: ThreadMessageLike
|
||||||
|
): ThreadMessageLike => {
|
||||||
|
if (afterToolCall && existingMessage) {
|
||||||
|
return cloneDeep(existingMessage);
|
||||||
|
}
|
||||||
|
return createEmptyResponse();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLastToolCall = (
|
||||||
|
message: ThreadMessageLike
|
||||||
|
): ToolCallPart | undefined => {
|
||||||
|
if (typeof message.content === "string") return undefined;
|
||||||
|
|
||||||
|
for (let i = message.content.length - 1; i >= 0; i -= 1) {
|
||||||
|
const part = message.content[i];
|
||||||
|
if (part.type === "tool-call") {
|
||||||
|
return part as ToolCallPart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
@ -0,0 +1,252 @@
|
|||||||
|
import type { ThreadMessageLike } from "@assistant-ui/react";
|
||||||
|
import type { Mistral } from "@mistralai/mistralai";
|
||||||
|
import type { Messages, Tool } from "@mistralai/mistralai/models/components";
|
||||||
|
import type { Model, TMCPItem, TProvider } from "@/lib/types";
|
||||||
|
import { AbstractBaseProvider, type TData, type TErrorData } from "../base";
|
||||||
|
import { extractErrorMessage, getErrorStatus, ProviderErrors } from "../errors";
|
||||||
|
import { CREATE_TITLE_SYSTEM_PROMPT } from "../prompts";
|
||||||
|
import {
|
||||||
|
getChoiceFromEvent,
|
||||||
|
handleTextContent,
|
||||||
|
handleToolCall,
|
||||||
|
} from "./handlers";
|
||||||
|
import {
|
||||||
|
createClient,
|
||||||
|
createErrorResponse,
|
||||||
|
createResponseShell,
|
||||||
|
getLastToolCall,
|
||||||
|
} from "./helpers";
|
||||||
|
import { mistralInfo } from "./info";
|
||||||
|
import type { StreamResult } from "./types";
|
||||||
|
import {
|
||||||
|
convertMessagesToModelFormat,
|
||||||
|
convertToolsToModelFormat,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
|
class MistralProvider extends AbstractBaseProvider<Tool, Messages, Mistral> {
|
||||||
|
setProvider = (provider: TProvider): void => {
|
||||||
|
this.provider = provider;
|
||||||
|
this.client = createClient(provider.key, provider.baseUrl);
|
||||||
|
|
||||||
|
if (provider.key) this.setApiKey(provider.key);
|
||||||
|
if (provider.baseUrl) this.setUrl(provider.baseUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
setPrevMessages = (prevMessages: ThreadMessageLike[]): void => {
|
||||||
|
this.prevMessages = convertMessagesToModelFormat(prevMessages);
|
||||||
|
};
|
||||||
|
|
||||||
|
setTools = (tools: TMCPItem[]): void => {
|
||||||
|
this.tools = convertToolsToModelFormat(tools);
|
||||||
|
};
|
||||||
|
|
||||||
|
private pushToHistory = (message: ThreadMessageLike): void => {
|
||||||
|
const converted = convertMessagesToModelFormat([message]);
|
||||||
|
this.prevMessages.push(...converted);
|
||||||
|
};
|
||||||
|
|
||||||
|
private pushToHistorySliced = (
|
||||||
|
responseMessage: ThreadMessageLike,
|
||||||
|
originalMessage: ThreadMessageLike
|
||||||
|
): void => {
|
||||||
|
if (typeof responseMessage.content === "string") return;
|
||||||
|
if (typeof originalMessage.content === "string") return;
|
||||||
|
|
||||||
|
const newContent = responseMessage.content.slice(
|
||||||
|
originalMessage.content.length
|
||||||
|
);
|
||||||
|
this.pushToHistory({ ...responseMessage, content: newContent });
|
||||||
|
};
|
||||||
|
|
||||||
|
async createChatName(message: string): Promise<string> {
|
||||||
|
if (!this.client) return "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.client.chat.complete({
|
||||||
|
model: this.modelKey,
|
||||||
|
messages: [
|
||||||
|
{ role: "system", content: CREATE_TITLE_SYSTEM_PROMPT },
|
||||||
|
{ role: "user", content: message },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = response.choices?.[0]?.message?.content;
|
||||||
|
if (typeof content === "string") {
|
||||||
|
return content.substring(0, 25);
|
||||||
|
}
|
||||||
|
|
||||||
|
return message.substring(0, 25);
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getModelKey = (withThinking?: boolean): string => {
|
||||||
|
if (!withThinking) return this.modelKey;
|
||||||
|
|
||||||
|
const reasoningModel = mistralInfo.reasoningModels.find(
|
||||||
|
([level, _modelKeyy]) => this.modelKey.includes(level)
|
||||||
|
);
|
||||||
|
|
||||||
|
return reasoningModel ? reasoningModel[1] : this.modelKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
async *sendMessage(
|
||||||
|
messages: ThreadMessageLike[],
|
||||||
|
afterToolCall?: boolean,
|
||||||
|
previousMessage?: ThreadMessageLike,
|
||||||
|
withThinking?: boolean
|
||||||
|
): AsyncGenerator<StreamResult> {
|
||||||
|
if (!this.client) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const convertedMessages = convertMessagesToModelFormat(messages);
|
||||||
|
this.prevMessages.push(...convertedMessages);
|
||||||
|
|
||||||
|
const model = this.getModelKey(withThinking);
|
||||||
|
|
||||||
|
const allMessages: Messages[] = [
|
||||||
|
{ role: "system", content: this.systemPrompt },
|
||||||
|
...this.prevMessages,
|
||||||
|
];
|
||||||
|
|
||||||
|
const stream = await this.client.chat.stream({
|
||||||
|
model,
|
||||||
|
messages: allMessages,
|
||||||
|
tools: this.tools.length > 0 ? this.tools : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
let responseMessage = createResponseShell(afterToolCall, previousMessage);
|
||||||
|
|
||||||
|
for await (const event of stream) {
|
||||||
|
// Handle stop flag
|
||||||
|
if (this.stopFlag) {
|
||||||
|
this.stopFlag = false;
|
||||||
|
this.pushToHistory(responseMessage);
|
||||||
|
yield { isEnd: true, responseMessage };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const choice = getChoiceFromEvent(event);
|
||||||
|
if (!choice) continue;
|
||||||
|
|
||||||
|
const delta = choice.delta;
|
||||||
|
|
||||||
|
// Handle tool call
|
||||||
|
responseMessage = handleToolCall(delta, responseMessage);
|
||||||
|
|
||||||
|
// Handle text content
|
||||||
|
responseMessage = handleTextContent(delta, responseMessage);
|
||||||
|
|
||||||
|
// Handle finish
|
||||||
|
if (choice.finishReason) {
|
||||||
|
if (afterToolCall && previousMessage) {
|
||||||
|
this.pushToHistorySliced(responseMessage, previousMessage);
|
||||||
|
} else {
|
||||||
|
this.pushToHistory(responseMessage);
|
||||||
|
}
|
||||||
|
yield { isEnd: true, responseMessage };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield responseMessage;
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
console.error("Mistral sendMessage error:", _error);
|
||||||
|
yield { isEnd: true, responseMessage: createErrorResponse(_error) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async *sendMessageAfterToolCall(
|
||||||
|
message: ThreadMessageLike,
|
||||||
|
withThinking?: boolean
|
||||||
|
): AsyncGenerator<StreamResult> {
|
||||||
|
if (typeof message.content === "string") return message;
|
||||||
|
|
||||||
|
const lastToolCall = getLastToolCall(message);
|
||||||
|
if (!lastToolCall) return message;
|
||||||
|
|
||||||
|
const toolResult = {
|
||||||
|
role: "tool" as const,
|
||||||
|
content:
|
||||||
|
typeof lastToolCall.result === "string"
|
||||||
|
? lastToolCall.result
|
||||||
|
: JSON.stringify(lastToolCall.result),
|
||||||
|
toolCallId: lastToolCall.toolCallId ?? "",
|
||||||
|
name: lastToolCall.toolName,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.prevMessages.push(toolResult as Messages);
|
||||||
|
yield* this.sendMessage([], true, message, withThinking);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBaseUrl = (): string => mistralInfo.baseUrl;
|
||||||
|
|
||||||
|
getName = (): string => mistralInfo.name;
|
||||||
|
|
||||||
|
checkProvider = async (data: TData): Promise<boolean | TErrorData> => {
|
||||||
|
const client = createClient(data.apiKey, data.url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.models.list();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
const status = getErrorStatus(error);
|
||||||
|
|
||||||
|
if (
|
||||||
|
status === 0 ||
|
||||||
|
(error && typeof error === "object" && "cause" in error)
|
||||||
|
) {
|
||||||
|
return ProviderErrors.invalidUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 401) {
|
||||||
|
return ProviderErrors.invalidKey(extractErrorMessage(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 404) {
|
||||||
|
return ProviderErrors.invalidUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.apiKey
|
||||||
|
? ProviderErrors.invalidKey()
|
||||||
|
: ProviderErrors.emptyKey();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getProviderModels = async (data: TData): Promise<Model[]> => {
|
||||||
|
const client = createClient(data.apiKey, data.url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await client.models.list();
|
||||||
|
const models = response.data ?? [];
|
||||||
|
|
||||||
|
const result: Model[] = [];
|
||||||
|
|
||||||
|
for (const model of models) {
|
||||||
|
const matchesFilter =
|
||||||
|
mistralInfo.modelFilters.length === 0 ||
|
||||||
|
mistralInfo.modelFilters.some((f) => model.id?.includes(f));
|
||||||
|
|
||||||
|
if (!matchesFilter) continue;
|
||||||
|
|
||||||
|
const displayName =
|
||||||
|
mistralInfo.modelNames[model.id ?? ""] ?? model.id ?? "Unknown";
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
id: model.id ?? "",
|
||||||
|
name: displayName,
|
||||||
|
provider: "mistral" as const,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mistralProvider = new MistralProvider();
|
||||||
|
|
||||||
|
export { MistralProvider, mistralProvider };
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
export const mistralInfo = {
|
||||||
|
name: "Mistral",
|
||||||
|
baseUrl: "https://api.mistral.ai",
|
||||||
|
modelFilters: [
|
||||||
|
"mistral-large-latest",
|
||||||
|
"mistral-medium-latest",
|
||||||
|
"mistral-small-latest",
|
||||||
|
] as string[],
|
||||||
|
modelNames: {
|
||||||
|
"mistral-large-latest": "Mistral Large",
|
||||||
|
"mistral-medium-latest": "Mistral Medium",
|
||||||
|
"mistral-small-latest": "Mistral Small",
|
||||||
|
} as Record<string, string>,
|
||||||
|
reasoningModels: [
|
||||||
|
["small", "magistral-small-latest"],
|
||||||
|
["medium", "magistral-medium-latest"],
|
||||||
|
["large", "magistral-medium-latest"],
|
||||||
|
] as [string, string][],
|
||||||
|
};
|
||||||
@ -0,0 +1,399 @@
|
|||||||
|
import type { ThreadMessageLike } from "@assistant-ui/react";
|
||||||
|
import type {
|
||||||
|
CompletionEvent,
|
||||||
|
DeltaMessage,
|
||||||
|
} from "@mistralai/mistralai/models/components";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
getChoiceFromEvent,
|
||||||
|
handleTextContent,
|
||||||
|
handleToolCall,
|
||||||
|
} from "../handlers";
|
||||||
|
|
||||||
|
// Helper to create partial event objects for testing
|
||||||
|
const createEvent = <T>(partial: Partial<T>) => partial as T;
|
||||||
|
|
||||||
|
describe("mistral handlers", () => {
|
||||||
|
// ==========================================================================
|
||||||
|
// handleToolCall
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
describe("handleToolCall", () => {
|
||||||
|
it("should return unchanged message when no tool calls", () => {
|
||||||
|
const delta = createEvent<DeltaMessage>({
|
||||||
|
toolCalls: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevMessage: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = handleToolCall(delta, prevMessage);
|
||||||
|
|
||||||
|
expect(result).toEqual(prevMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return unchanged message when tool calls array is empty", () => {
|
||||||
|
const delta = createEvent<DeltaMessage>({
|
||||||
|
toolCalls: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevMessage: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = handleToolCall(delta, prevMessage);
|
||||||
|
|
||||||
|
expect(result).toEqual(prevMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add tool call to message content", () => {
|
||||||
|
const delta = createEvent<DeltaMessage>({
|
||||||
|
toolCalls: [
|
||||||
|
{
|
||||||
|
id: "tool_123",
|
||||||
|
function: {
|
||||||
|
name: "get_weather",
|
||||||
|
arguments: '{"city": "NYC"}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevMessage: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = handleToolCall(delta, prevMessage);
|
||||||
|
|
||||||
|
expect(result.content).toHaveLength(1);
|
||||||
|
expect(result.content).toEqual([
|
||||||
|
{
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId: "tool_123",
|
||||||
|
toolName: "get_weather",
|
||||||
|
args: { city: "NYC" },
|
||||||
|
argsText: '{"city": "NYC"}',
|
||||||
|
result: "",
|
||||||
|
parentId: "tool_123",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle tool call with empty arguments", () => {
|
||||||
|
const delta = createEvent<DeltaMessage>({
|
||||||
|
toolCalls: [
|
||||||
|
{
|
||||||
|
id: "tool_456",
|
||||||
|
function: {
|
||||||
|
name: "no_args_tool",
|
||||||
|
arguments: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevMessage: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = handleToolCall(delta, prevMessage);
|
||||||
|
|
||||||
|
expect(result.content).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
(result.content as unknown as Array<{ args: unknown }>)[0].args
|
||||||
|
).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle tool call with undefined id", () => {
|
||||||
|
const delta = createEvent<DeltaMessage>({
|
||||||
|
toolCalls: [
|
||||||
|
{
|
||||||
|
id: undefined,
|
||||||
|
function: {
|
||||||
|
name: "test_tool",
|
||||||
|
arguments: "{}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevMessage: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = handleToolCall(delta, prevMessage);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(result.content as unknown as Array<{ toolCallId: string }>)[0]
|
||||||
|
.toolCallId
|
||||||
|
).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle tool call with undefined function name", () => {
|
||||||
|
const delta = createEvent<DeltaMessage>({
|
||||||
|
toolCalls: [
|
||||||
|
{
|
||||||
|
id: "tool_789",
|
||||||
|
function: {
|
||||||
|
name: undefined as unknown as string,
|
||||||
|
arguments: "{}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevMessage: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = handleToolCall(delta, prevMessage);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(result.content as unknown as Array<{ toolName: string }>)[0].toolName
|
||||||
|
).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should append tool call to existing content", () => {
|
||||||
|
const delta = createEvent<DeltaMessage>({
|
||||||
|
toolCalls: [
|
||||||
|
{
|
||||||
|
id: "tool_new",
|
||||||
|
function: {
|
||||||
|
name: "new_tool",
|
||||||
|
arguments: "{}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevMessage: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "Let me check that" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = handleToolCall(delta, prevMessage);
|
||||||
|
|
||||||
|
expect(result.content).toHaveLength(2);
|
||||||
|
expect(
|
||||||
|
(result.content as unknown as Array<{ type: string }>)[0].type
|
||||||
|
).toBe("text");
|
||||||
|
expect(
|
||||||
|
(result.content as unknown as Array<{ type: string }>)[1].type
|
||||||
|
).toBe("tool-call");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return unchanged message when content is string", () => {
|
||||||
|
const delta = createEvent<DeltaMessage>({
|
||||||
|
toolCalls: [
|
||||||
|
{
|
||||||
|
id: "tool_123",
|
||||||
|
function: {
|
||||||
|
name: "test_tool",
|
||||||
|
arguments: "{}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevMessage: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: "string content",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = handleToolCall(delta, prevMessage);
|
||||||
|
|
||||||
|
expect(result).toEqual(prevMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle object arguments", () => {
|
||||||
|
const delta = createEvent<DeltaMessage>({
|
||||||
|
toolCalls: [
|
||||||
|
{
|
||||||
|
id: "tool_obj",
|
||||||
|
function: {
|
||||||
|
name: "obj_args_tool",
|
||||||
|
arguments: { key: "value" } as unknown as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevMessage: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = handleToolCall(delta, prevMessage);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(result.content as unknown as Array<{ args: unknown }>)[0].args
|
||||||
|
).toEqual({
|
||||||
|
key: "value",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// handleTextContent
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
describe("handleTextContent", () => {
|
||||||
|
it("should return unchanged message when delta content is not string", () => {
|
||||||
|
const delta = createEvent<DeltaMessage>({
|
||||||
|
content: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevMessage: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = handleTextContent(delta, prevMessage);
|
||||||
|
|
||||||
|
expect(result).toEqual(prevMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return unchanged message when response content is string", () => {
|
||||||
|
const delta = createEvent<DeltaMessage>({
|
||||||
|
content: "new text",
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevMessage: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: "string content",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = handleTextContent(delta, prevMessage);
|
||||||
|
|
||||||
|
expect(result).toEqual(prevMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add new text part when content is empty", () => {
|
||||||
|
const delta = createEvent<DeltaMessage>({
|
||||||
|
content: "Hello",
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevMessage: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = handleTextContent(delta, prevMessage);
|
||||||
|
|
||||||
|
expect(result.content).toEqual([{ type: "text", text: "Hello" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should append to existing text part", () => {
|
||||||
|
const delta = createEvent<DeltaMessage>({
|
||||||
|
content: " world",
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevMessage: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "Hello" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = handleTextContent(delta, prevMessage);
|
||||||
|
|
||||||
|
expect(result.content).toEqual([{ type: "text", text: "Hello world" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add new text part when last content is not text", () => {
|
||||||
|
const delta = createEvent<DeltaMessage>({
|
||||||
|
content: "New text",
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevMessage: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId: "tool_123",
|
||||||
|
toolName: "test",
|
||||||
|
args: {},
|
||||||
|
argsText: "{}",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = handleTextContent(delta, prevMessage);
|
||||||
|
|
||||||
|
expect(result.content).toHaveLength(2);
|
||||||
|
expect(
|
||||||
|
(result.content as unknown as Array<{ type: string }>)[1].type
|
||||||
|
).toBe("text");
|
||||||
|
expect(
|
||||||
|
(result.content as unknown as Array<{ text?: string }>)[1].text
|
||||||
|
).toBe("New text");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not mutate original message", () => {
|
||||||
|
const delta = createEvent<DeltaMessage>({
|
||||||
|
content: " world",
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevMessage: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "Hello" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
handleTextContent(delta, prevMessage);
|
||||||
|
|
||||||
|
expect(prevMessage.content).toEqual([{ type: "text", text: "Hello" }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// getChoiceFromEvent
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
describe("getChoiceFromEvent", () => {
|
||||||
|
it("should return first choice from event", () => {
|
||||||
|
const event = createEvent<CompletionEvent>({
|
||||||
|
data: {
|
||||||
|
id: "test-id",
|
||||||
|
model: "mistral-small",
|
||||||
|
choices: [
|
||||||
|
{ index: 0, delta: { content: "Hello" }, finishReason: null },
|
||||||
|
{ index: 1, delta: { content: "World" }, finishReason: null },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getChoiceFromEvent(event);
|
||||||
|
|
||||||
|
expect(result?.delta.content).toBe("Hello");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined when no choices", () => {
|
||||||
|
const event = createEvent<CompletionEvent>({
|
||||||
|
data: {
|
||||||
|
id: "test-id",
|
||||||
|
model: "mistral-small",
|
||||||
|
choices: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getChoiceFromEvent(event);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined when data is undefined", () => {
|
||||||
|
const event = createEvent<CompletionEvent>({
|
||||||
|
data: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getChoiceFromEvent(event);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,277 @@
|
|||||||
|
import type { ThreadMessageLike } from "@assistant-ui/react";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
createClient,
|
||||||
|
createEmptyResponse,
|
||||||
|
createErrorResponse,
|
||||||
|
createResponseShell,
|
||||||
|
getLastToolCall,
|
||||||
|
} from "../helpers";
|
||||||
|
|
||||||
|
// Mock the Mistral SDK
|
||||||
|
vi.mock("@mistralai/mistralai", () => {
|
||||||
|
return {
|
||||||
|
Mistral: class MockMistral {
|
||||||
|
apiKey: string;
|
||||||
|
serverURL: string | undefined;
|
||||||
|
constructor(config: { apiKey: string; serverURL?: string }) {
|
||||||
|
this.apiKey = config.apiKey;
|
||||||
|
this.serverURL = config.serverURL;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("mistral helpers", () => {
|
||||||
|
// ==========================================================================
|
||||||
|
// createClient
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
describe("createClient", () => {
|
||||||
|
it("should create client with apiKey and baseUrl", () => {
|
||||||
|
const client = createClient("test-api-key", "https://api.mistral.ai");
|
||||||
|
|
||||||
|
expect(client).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create client with empty apiKey when not provided", () => {
|
||||||
|
const client = createClient(undefined, "https://api.mistral.ai");
|
||||||
|
|
||||||
|
expect(client).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create client with undefined serverURL", () => {
|
||||||
|
const client = createClient("test-key", undefined);
|
||||||
|
|
||||||
|
expect(client).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create client with no arguments", () => {
|
||||||
|
const client = createClient();
|
||||||
|
|
||||||
|
expect(client).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// createEmptyResponse
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
describe("createEmptyResponse", () => {
|
||||||
|
it("should create empty assistant response", () => {
|
||||||
|
const result = createEmptyResponse();
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// createErrorResponse
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
describe("createErrorResponse", () => {
|
||||||
|
it("should create error response from Error instance", () => {
|
||||||
|
const error = new Error("Something went wrong");
|
||||||
|
const result = createErrorResponse(error);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: Something went wrong",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create error response from string", () => {
|
||||||
|
const result = createErrorResponse("String error");
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: String error",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create error response from object", () => {
|
||||||
|
const result = createErrorResponse({
|
||||||
|
code: 500,
|
||||||
|
message: "Server error",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.content).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
(result.content as unknown as Array<{ text: string }>)[0].text
|
||||||
|
).toContain("Error:");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create error response from null", () => {
|
||||||
|
const result = createErrorResponse(null);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: null",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create error response from undefined", () => {
|
||||||
|
const result = createErrorResponse(undefined);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: undefined",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// createResponseShell
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
describe("createResponseShell", () => {
|
||||||
|
it("should return empty response when afterToolCall is false", () => {
|
||||||
|
const result = createResponseShell(false, undefined);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty response when existingMessage is undefined", () => {
|
||||||
|
const result = createResponseShell(true, undefined);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return cloned existing message when afterToolCall and existingMessage provided", () => {
|
||||||
|
const existingMessage: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "Hello" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = createResponseShell(true, existingMessage);
|
||||||
|
|
||||||
|
expect(result).toEqual(existingMessage);
|
||||||
|
expect(result).not.toBe(existingMessage); // Should be a clone
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty response when called with no arguments", () => {
|
||||||
|
const result = createResponseShell();
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// getLastToolCall
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
describe("getLastToolCall", () => {
|
||||||
|
it("should return undefined for string content", () => {
|
||||||
|
const message: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: "Just text",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getLastToolCall(message);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined when no tool calls exist", () => {
|
||||||
|
const message: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "Hello" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getLastToolCall(message);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the last tool call", () => {
|
||||||
|
const message: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId: "tool_1",
|
||||||
|
toolName: "first_tool",
|
||||||
|
args: {},
|
||||||
|
argsText: "{}",
|
||||||
|
},
|
||||||
|
{ type: "text", text: "Some text" },
|
||||||
|
{
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId: "tool_2",
|
||||||
|
toolName: "last_tool",
|
||||||
|
args: { key: "value" },
|
||||||
|
argsText: '{"key":"value"}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getLastToolCall(message);
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result?.toolCallId).toBe("tool_2");
|
||||||
|
expect(result?.toolName).toBe("last_tool");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return tool call when it is the only content", () => {
|
||||||
|
const message: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId: "tool_only",
|
||||||
|
toolName: "single_tool",
|
||||||
|
args: {},
|
||||||
|
argsText: "{}",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getLastToolCall(message);
|
||||||
|
|
||||||
|
expect(result?.toolCallId).toBe("tool_only");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined for empty content array", () => {
|
||||||
|
const message: ThreadMessageLike = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getLastToolCall(message);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,415 @@
|
|||||||
|
import type { ThreadMessageLike } from "@assistant-ui/react";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
convertMessagesToModelFormat,
|
||||||
|
convertToolsToModelFormat,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
describe("mistral utils", () => {
|
||||||
|
// ==========================================================================
|
||||||
|
// convertToolsToModelFormat
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
describe("convertToolsToModelFormat", () => {
|
||||||
|
it("should convert tools to Mistral format", () => {
|
||||||
|
const tools = [
|
||||||
|
{
|
||||||
|
name: "get_weather",
|
||||||
|
description: "Get current weather",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
city: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["city"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = convertToolsToModelFormat(tools);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]).toEqual({
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "get_weather",
|
||||||
|
description: "Get current weather",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
city: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["city"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty tools array", () => {
|
||||||
|
const result = convertToolsToModelFormat([]);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should convert multiple tools", () => {
|
||||||
|
const tools = [
|
||||||
|
{
|
||||||
|
name: "tool_1",
|
||||||
|
description: "First tool",
|
||||||
|
inputSchema: { properties: {} },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tool_2",
|
||||||
|
description: "Second tool",
|
||||||
|
inputSchema: { properties: {} },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = convertToolsToModelFormat(tools);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0].function.name).toBe("tool_1");
|
||||||
|
expect(result[1].function.name).toBe("tool_2");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// convertMessagesToModelFormat
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
describe("convertMessagesToModelFormat", () => {
|
||||||
|
describe("user messages", () => {
|
||||||
|
it("should convert user message with string content", () => {
|
||||||
|
const messages: ThreadMessageLike[] = [
|
||||||
|
{ role: "user", content: "Hello" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = convertMessagesToModelFormat(messages);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: "Hello" }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should convert user message with text parts", () => {
|
||||||
|
const messages: ThreadMessageLike[] = [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: "Hello world" }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = convertMessagesToModelFormat(messages);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: "Hello world" }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should convert user message with file attachments", () => {
|
||||||
|
const messages: ThreadMessageLike[] = [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: "Check this file",
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
type: "file",
|
||||||
|
name: "test.txt",
|
||||||
|
content: "file content",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = convertMessagesToModelFormat(messages);
|
||||||
|
|
||||||
|
expect(result[0].content).toContainEqual({
|
||||||
|
type: "text",
|
||||||
|
text: "File: test.txt: file content",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should convert user message with image attachments", () => {
|
||||||
|
const messages: ThreadMessageLike[] = [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: "Check this image",
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
name: "photo.png",
|
||||||
|
content: "base64data",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = convertMessagesToModelFormat(messages);
|
||||||
|
|
||||||
|
expect(result[0].content).toContainEqual({
|
||||||
|
type: "text",
|
||||||
|
text: "Image: photo.png: base64data",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle user message with image content part", () => {
|
||||||
|
const messages: ThreadMessageLike[] = [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "Look at this" },
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
image: "base64imagedata",
|
||||||
|
filename: "screenshot.png",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = convertMessagesToModelFormat(messages);
|
||||||
|
|
||||||
|
expect(result[0].content).toContainEqual({
|
||||||
|
type: "text",
|
||||||
|
text: "Image: screenshot.png: base64imagedata",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle user message with file content part", () => {
|
||||||
|
const messages: ThreadMessageLike[] = [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "Check this" },
|
||||||
|
{
|
||||||
|
type: "file",
|
||||||
|
data: "file data here",
|
||||||
|
filename: "doc.txt",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = convertMessagesToModelFormat(messages);
|
||||||
|
|
||||||
|
expect(result[0].content).toContainEqual({
|
||||||
|
type: "text",
|
||||||
|
text: "File: doc.txt: file data here",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle tool result in user message", () => {
|
||||||
|
const messages: ThreadMessageLike[] = [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId: "tool_123",
|
||||||
|
toolName: "get_weather",
|
||||||
|
args: {},
|
||||||
|
argsText: "{}",
|
||||||
|
result: "Sunny, 72F",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = convertMessagesToModelFormat(messages);
|
||||||
|
|
||||||
|
// Should have user message and tool message
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[1]).toEqual({
|
||||||
|
role: "tool",
|
||||||
|
content: "Sunny, 72F",
|
||||||
|
name: "get_weather",
|
||||||
|
toolCallId: "tool_123",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("assistant messages", () => {
|
||||||
|
it("should convert assistant message with string content", () => {
|
||||||
|
const messages: ThreadMessageLike[] = [
|
||||||
|
{ role: "assistant", content: "Hello!" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = convertMessagesToModelFormat(messages);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "Hello!" }],
|
||||||
|
toolCalls: undefined,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should convert assistant message with text parts", () => {
|
||||||
|
const messages: ThreadMessageLike[] = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "Response" }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = convertMessagesToModelFormat(messages);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "Response" }],
|
||||||
|
toolCalls: undefined,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should convert assistant message with tool call", () => {
|
||||||
|
const messages: ThreadMessageLike[] = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId: "tool_123",
|
||||||
|
toolName: "get_weather",
|
||||||
|
args: { city: "NYC" },
|
||||||
|
argsText: '{"city":"NYC"}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = convertMessagesToModelFormat(messages);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
toolCalls: [
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "get_weather",
|
||||||
|
arguments: '{"city":"NYC"}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should convert assistant message with reasoning part", () => {
|
||||||
|
const messages: ThreadMessageLike[] = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "reasoning",
|
||||||
|
text: "Let me think about this",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = convertMessagesToModelFormat(messages);
|
||||||
|
|
||||||
|
expect(result[0].content).toContainEqual({
|
||||||
|
type: "thinking",
|
||||||
|
thinking: [{ type: "text", text: "Let me think about this" }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle tool call with args object when argsText is empty", () => {
|
||||||
|
const messages: ThreadMessageLike[] = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId: "tool_456",
|
||||||
|
toolName: "test_tool",
|
||||||
|
args: { key: "value" },
|
||||||
|
argsText: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = convertMessagesToModelFormat(messages);
|
||||||
|
|
||||||
|
expect(result[0].toolCalls[0].function.arguments).toBe(
|
||||||
|
'{"key":"value"}'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle tool call with empty args", () => {
|
||||||
|
const messages: ThreadMessageLike[] = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId: "tool_789",
|
||||||
|
toolName: "no_args",
|
||||||
|
args: undefined,
|
||||||
|
argsText: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = convertMessagesToModelFormat(messages);
|
||||||
|
|
||||||
|
expect(result[0].toolCalls[0].function.arguments).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("multiple messages", () => {
|
||||||
|
it("should convert conversation with multiple messages", () => {
|
||||||
|
const messages: ThreadMessageLike[] = [
|
||||||
|
{ role: "user", content: "Hi" },
|
||||||
|
{ role: "assistant", content: "Hello!" },
|
||||||
|
{ role: "user", content: "How are you?" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = convertMessagesToModelFormat(messages);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
expect(result[0].role).toBe("user");
|
||||||
|
expect(result[1].role).toBe("assistant");
|
||||||
|
expect(result[2].role).toBe("user");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle mixed content types in conversation", () => {
|
||||||
|
const messages: ThreadMessageLike[] = [
|
||||||
|
{ role: "user", content: "Check the weather" },
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "Let me check" },
|
||||||
|
{
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId: "tool_1",
|
||||||
|
toolName: "get_weather",
|
||||||
|
args: { city: "NYC" },
|
||||||
|
argsText: '{"city":"NYC"}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = convertMessagesToModelFormat(messages);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[1].toolCalls).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import type { ThreadMessageLike } from "@assistant-ui/react";
|
||||||
|
|
||||||
|
export type StreamResult =
|
||||||
|
| ThreadMessageLike
|
||||||
|
| { isEnd: true; responseMessage: ThreadMessageLike };
|
||||||
|
|
||||||
|
export type MessageArray = Exclude<ThreadMessageLike["content"], string>;
|
||||||
|
export type ToolCallElement = MessageArray extends ReadonlyArray<infer T>
|
||||||
|
? T
|
||||||
|
: never;
|
||||||
|
export type ToolCallPart = Extract<ToolCallElement, { type: "tool-call" }>;
|
||||||
@ -0,0 +1,134 @@
|
|||||||
|
import type { ThreadMessageLike } from "@assistant-ui/react";
|
||||||
|
import type {
|
||||||
|
AssistantMessage,
|
||||||
|
Messages,
|
||||||
|
Tool,
|
||||||
|
ToolMessage,
|
||||||
|
UserMessage,
|
||||||
|
} from "@mistralai/mistralai/models/components";
|
||||||
|
import type { TMCPItem } from "@/lib/types";
|
||||||
|
|
||||||
|
export const convertToolsToModelFormat = (tools: TMCPItem[]): Tool[] => {
|
||||||
|
return tools.map((tool) => ({
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
parameters: tool.inputSchema,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertMessagesToModelFormat = (
|
||||||
|
messages: ThreadMessageLike[]
|
||||||
|
): Messages[] => {
|
||||||
|
const result: Messages[] = [];
|
||||||
|
|
||||||
|
for (const msg of messages) {
|
||||||
|
if (msg.role === "user") {
|
||||||
|
const content: UserMessage["content"] = [];
|
||||||
|
const toolContent: ToolMessage = { content: null };
|
||||||
|
if (msg.attachments && msg.attachments.length > 0) {
|
||||||
|
msg.attachments.forEach((attachment) => {
|
||||||
|
if (attachment.type === "file") {
|
||||||
|
content.push({
|
||||||
|
type: "text",
|
||||||
|
text: `File: ${attachment.name}: ${attachment.content}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachment.type === "image") {
|
||||||
|
content.push({
|
||||||
|
type: "text",
|
||||||
|
text: `Image: ${attachment.name}: ${attachment.content}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof msg.content === "string") {
|
||||||
|
content.push({ type: "text", text: msg.content });
|
||||||
|
} else {
|
||||||
|
msg.content.forEach((part) => {
|
||||||
|
if (part.type === "text") {
|
||||||
|
content.push({ type: "text", text: part.text });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.type === "tool-call") {
|
||||||
|
toolContent.content = part.result || null;
|
||||||
|
toolContent.name = part.toolName;
|
||||||
|
toolContent.toolCallId = part.toolCallId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.type === "image") {
|
||||||
|
content.push({
|
||||||
|
type: "text",
|
||||||
|
text: `Image: ${part.filename}: ${part.image}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.type === "file") {
|
||||||
|
content.push({
|
||||||
|
type: "text",
|
||||||
|
text: `File: ${part.filename}: ${part.data}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
role: "user",
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (toolContent.content) {
|
||||||
|
result.push({
|
||||||
|
role: "tool",
|
||||||
|
...toolContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.role === "assistant") {
|
||||||
|
const content: AssistantMessage["content"] = [];
|
||||||
|
const toolCalls: AssistantMessage["toolCalls"] = [];
|
||||||
|
if (typeof msg.content === "string") {
|
||||||
|
content.push({ type: "text", text: msg.content });
|
||||||
|
} else {
|
||||||
|
msg.content.forEach((part) => {
|
||||||
|
if (part.type === "text") {
|
||||||
|
content.push({ type: "text", text: part.text });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.type === "reasoning") {
|
||||||
|
content.push({
|
||||||
|
type: "thinking",
|
||||||
|
thinking: [{ type: "text", text: part.text }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.type === "tool-call") {
|
||||||
|
toolCalls.push({
|
||||||
|
type: "function",
|
||||||
|
id: part.parentId,
|
||||||
|
function: {
|
||||||
|
name: part.toolName,
|
||||||
|
arguments:
|
||||||
|
part.argsText || part.args ? JSON.stringify(part.args) : "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
role: "assistant",
|
||||||
|
content,
|
||||||
|
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
@ -3,6 +3,7 @@ import { type AnthropicProvider, anthropicProvider } from "./anthropic";
|
|||||||
import { type DeepSeekProvider, deepseekProvider } from "./deepseek";
|
import { type DeepSeekProvider, deepseekProvider } from "./deepseek";
|
||||||
import { type GenAIProvider, genaiProvider } from "./genai";
|
import { type GenAIProvider, genaiProvider } from "./genai";
|
||||||
import { type LMStudioProvider, lmStudioProvider } from "./lm-studio";
|
import { type LMStudioProvider, lmStudioProvider } from "./lm-studio";
|
||||||
|
import { type MistralProvider, mistralProvider } from "./mistral";
|
||||||
import { type OllamaProvider, ollamaProvider } from "./ollama";
|
import { type OllamaProvider, ollamaProvider } from "./ollama";
|
||||||
import { type OpenAIProvider, openaiProvider } from "./openai";
|
import { type OpenAIProvider, openaiProvider } from "./openai";
|
||||||
import { type OpenRouterProvider, openrouterProvider } from "./openrouter";
|
import { type OpenRouterProvider, openrouterProvider } from "./openrouter";
|
||||||
@ -18,7 +19,8 @@ export type BaseProvider =
|
|||||||
| GenAIProvider
|
| GenAIProvider
|
||||||
| DeepSeekProvider
|
| DeepSeekProvider
|
||||||
| XAIProvider
|
| XAIProvider
|
||||||
| LMStudioProvider;
|
| LMStudioProvider
|
||||||
|
| MistralProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registry mapping provider types to their singleton instances.
|
* Registry mapping provider types to their singleton instances.
|
||||||
@ -34,6 +36,7 @@ export const providerRegistry: Record<ProviderType, BaseProvider> = {
|
|||||||
deepseek: deepseekProvider,
|
deepseek: deepseekProvider,
|
||||||
xai: xaiProvider,
|
xai: xaiProvider,
|
||||||
"lm-studio": lmStudioProvider,
|
"lm-studio": lmStudioProvider,
|
||||||
|
mistral: mistralProvider,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -168,6 +168,70 @@ export const createReasoningChunk = (
|
|||||||
],
|
],
|
||||||
}) as unknown as ChatCompletionChunk;
|
}) as unknown as ChatCompletionChunk;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Mistral Chunk Factories
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Mistral text delta event for streaming.
|
||||||
|
*/
|
||||||
|
export const createMistralTextDeltaEvent = (
|
||||||
|
content: string,
|
||||||
|
finishReason?: string
|
||||||
|
) => ({
|
||||||
|
data: {
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
delta: { content },
|
||||||
|
finishReason: finishReason ?? null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Mistral tool call event for streaming.
|
||||||
|
*/
|
||||||
|
export const createMistralToolCallEvent = (
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
args: string,
|
||||||
|
finishReason?: string
|
||||||
|
) => ({
|
||||||
|
data: {
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
delta: {
|
||||||
|
toolCalls: [
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
function: { name, arguments: args },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
finishReason: finishReason ?? null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Mistral finish event to end the stream.
|
||||||
|
*/
|
||||||
|
export const createMistralFinishEvent = () => ({
|
||||||
|
data: {
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
delta: {},
|
||||||
|
finishReason: "stop",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Message Factories
|
// Message Factories
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -197,17 +261,18 @@ export const createMessage = (
|
|||||||
export const createToolCallPart = (overrides?: {
|
export const createToolCallPart = (overrides?: {
|
||||||
toolCallId?: string;
|
toolCallId?: string;
|
||||||
toolName?: string;
|
toolName?: string;
|
||||||
args?: Record<string, unknown>;
|
args?: Record<string, string | number | boolean | null>;
|
||||||
argsText?: string;
|
argsText?: string;
|
||||||
result?: unknown;
|
result?: unknown;
|
||||||
}) => ({
|
}) =>
|
||||||
type: "tool-call" as const,
|
({
|
||||||
toolCallId: overrides?.toolCallId ?? "call_123",
|
type: "tool-call" as const,
|
||||||
toolName: overrides?.toolName ?? "test_tool",
|
toolCallId: overrides?.toolCallId ?? "call_123",
|
||||||
args: overrides?.args ?? {},
|
toolName: overrides?.toolName ?? "test_tool",
|
||||||
argsText: overrides?.argsText ?? "{}",
|
args: overrides?.args ?? {},
|
||||||
...(overrides?.result !== undefined && { result: overrides.result }),
|
argsText: overrides?.argsText ?? "{}",
|
||||||
});
|
...(overrides?.result !== undefined && { result: overrides.result }),
|
||||||
|
}) as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a text content part.
|
* Creates a text content part.
|
||||||
@ -241,7 +306,17 @@ export const createTestProvider = (
|
|||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
}
|
}
|
||||||
) => ({
|
) => ({
|
||||||
type: type as "openai" | "anthropic" | "genai",
|
type: type as
|
||||||
|
| "openai"
|
||||||
|
| "anthropic"
|
||||||
|
| "genai"
|
||||||
|
| "mistral"
|
||||||
|
| "deepseek"
|
||||||
|
| "together"
|
||||||
|
| "xai"
|
||||||
|
| "openrouter"
|
||||||
|
| "ollama"
|
||||||
|
| "lm-studio",
|
||||||
name: overrides?.name ?? type,
|
name: overrides?.name ?? type,
|
||||||
key: overrides?.key ?? "test-key",
|
key: overrides?.key ?? "test-key",
|
||||||
baseUrl: overrides?.baseUrl ?? `https://api.${type}.com/v1`,
|
baseUrl: overrides?.baseUrl ?? `https://api.${type}.com/v1`,
|
||||||
|
|||||||
Reference in New Issue
Block a user