ba93da9a93
Stream implementations move from src/providers/ to src/api/, renamed by API id (anthropic.ts -> anthropic-messages.ts, google.ts -> google-generative-ai.ts, mistral.ts -> mistral-conversations.ts, amazon-bedrock.ts -> bedrock-converse-stream.ts). Every module now exports exactly stream/streamSimple; shared helpers move alongside. New ProviderStreams dispatch contract in types.ts, lazyApi() wrapper in api/lazy.ts, and one .lazy.ts wrapper per API. Bedrock's wrapper keeps the node-only variable-specifier import and setBedrockProviderModule() (now taking ProviderStreams). providers/register-builtins.ts deleted; interim until the compat entrypoint lands, builtin api-registry registration lives in stream.ts and lazy wrappers are exported from the root barrel. Old per-API lazy exports (streamAnthropic, ...) are gone; package.json subpaths retarget to dist/api/.
103 lines
2.8 KiB
TypeScript
103 lines
2.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { convertMessages } from "../src/api/openai-completions.ts";
|
|
import { getModel } from "../src/models.ts";
|
|
import type {
|
|
AssistantMessage,
|
|
Context,
|
|
Model,
|
|
OpenAICompletionsCompat,
|
|
ToolResultMessage,
|
|
Usage,
|
|
} from "../src/types.ts";
|
|
|
|
const emptyUsage: Usage = {
|
|
input: 0,
|
|
output: 0,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
totalTokens: 0,
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
};
|
|
|
|
const compat: Required<OpenAICompletionsCompat> = {
|
|
supportsStore: true,
|
|
supportsDeveloperRole: true,
|
|
supportsReasoningEffort: true,
|
|
supportsUsageInStreaming: true,
|
|
maxTokensField: "max_completion_tokens",
|
|
requiresToolResultName: false,
|
|
requiresAssistantAfterToolResult: false,
|
|
requiresThinkingAsText: false,
|
|
requiresReasoningContentOnAssistantMessages: false,
|
|
thinkingFormat: "openai",
|
|
openRouterRouting: {},
|
|
vercelGatewayRouting: {},
|
|
zaiToolStream: false,
|
|
supportsStrictMode: true,
|
|
cacheControlFormat: "anthropic",
|
|
sendSessionAffinityHeaders: false,
|
|
supportsLongCacheRetention: true,
|
|
};
|
|
|
|
function buildToolResult(toolCallId: string, timestamp: number): ToolResultMessage {
|
|
return {
|
|
role: "toolResult",
|
|
toolCallId,
|
|
toolName: "read",
|
|
content: [
|
|
{ type: "text", text: "Read image file [image/png]" },
|
|
{ type: "image", data: "ZmFrZQ==", mimeType: "image/png" },
|
|
],
|
|
isError: false,
|
|
timestamp,
|
|
};
|
|
}
|
|
|
|
describe("openai-completions convertMessages", () => {
|
|
it("batches tool-result images after consecutive tool results", () => {
|
|
const { compat: _compat, ...baseModel } = getModel("openai", "gpt-4o-mini");
|
|
const model: Model<"openai-completions"> = {
|
|
...baseModel,
|
|
api: "openai-completions",
|
|
input: ["text", "image"],
|
|
};
|
|
|
|
const now = Date.now();
|
|
const assistantMessage: AssistantMessage = {
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "toolCall", id: "tool-1", name: "read", arguments: { path: "img-1.png" } },
|
|
{ type: "toolCall", id: "tool-2", name: "read", arguments: { path: "img-2.png" } },
|
|
],
|
|
api: model.api,
|
|
provider: model.provider,
|
|
model: model.id,
|
|
usage: emptyUsage,
|
|
stopReason: "toolUse",
|
|
timestamp: now,
|
|
};
|
|
|
|
const context: Context = {
|
|
messages: [
|
|
{ role: "user", content: "Read the images", timestamp: now - 2 },
|
|
assistantMessage,
|
|
buildToolResult("tool-1", now + 1),
|
|
buildToolResult("tool-2", now + 2),
|
|
],
|
|
};
|
|
|
|
const messages = convertMessages(model, context, compat);
|
|
const roles = messages.map((message) => message.role);
|
|
expect(roles).toEqual(["user", "assistant", "tool", "tool", "user"]);
|
|
|
|
const imageMessage = messages[messages.length - 1];
|
|
expect(imageMessage.role).toBe("user");
|
|
expect(Array.isArray(imageMessage.content)).toBe(true);
|
|
|
|
const imageParts = (imageMessage.content as Array<{ type?: string }>).filter(
|
|
(part) => part?.type === "image_url",
|
|
);
|
|
expect(imageParts.length).toBe(2);
|
|
});
|
|
});
|