mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
Together no longer lists Kimi K2.6, which broke type checks against the hydrated catalog. Tests use Kimi K3 and the Together default model now points at it.
822 lines
27 KiB
TypeScript
822 lines
27 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { complete, getModel } from "../src/compat.ts";
|
|
import type { Api, AssistantMessage, Context, Model, StreamOptions, UserMessage } from "../src/types.ts";
|
|
|
|
type StreamOptionsWithExtras = StreamOptions & Record<string, unknown>;
|
|
|
|
import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts";
|
|
import { hasBedrockCredentials } from "./bedrock-utils.ts";
|
|
import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.ts";
|
|
import { resolveApiKey } from "./oauth.ts";
|
|
|
|
// Resolve OAuth tokens at module level (async, runs before tests)
|
|
const oauthTokens = await Promise.all([
|
|
resolveApiKey("anthropic"),
|
|
resolveApiKey("github-copilot"),
|
|
resolveApiKey("openai-codex"),
|
|
]);
|
|
const [anthropicOAuthToken, githubCopilotToken, openaiCodexToken] = oauthTokens;
|
|
|
|
async function testEmptyMessage<TApi extends Api>(llm: Model<TApi>, options: StreamOptionsWithExtras = {}) {
|
|
// Test with completely empty content array
|
|
const emptyMessage: UserMessage = {
|
|
role: "user",
|
|
content: [],
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
const context: Context = {
|
|
messages: [emptyMessage],
|
|
};
|
|
|
|
const response = await complete(llm, context, options);
|
|
|
|
// Should either handle gracefully or return an error
|
|
expect(response).toBeDefined();
|
|
expect(response.role).toBe("assistant");
|
|
// Should handle empty string gracefully
|
|
if (response.stopReason === "error") {
|
|
expect(response.errorMessage).toBeDefined();
|
|
} else {
|
|
expect(response.content).toBeDefined();
|
|
}
|
|
}
|
|
|
|
async function testEmptyStringMessage<TApi extends Api>(llm: Model<TApi>, options: StreamOptionsWithExtras = {}) {
|
|
// Test with empty string content
|
|
const context: Context = {
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: "",
|
|
timestamp: Date.now(),
|
|
},
|
|
],
|
|
};
|
|
|
|
const response = await complete(llm, context, options);
|
|
|
|
expect(response).toBeDefined();
|
|
expect(response.role).toBe("assistant");
|
|
|
|
// Should handle empty string gracefully
|
|
if (response.stopReason === "error") {
|
|
expect(response.errorMessage).toBeDefined();
|
|
} else {
|
|
expect(response.content).toBeDefined();
|
|
}
|
|
}
|
|
|
|
async function testWhitespaceOnlyMessage<TApi extends Api>(llm: Model<TApi>, options: StreamOptionsWithExtras = {}) {
|
|
// Test with whitespace-only content
|
|
const context: Context = {
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: " \n\t ",
|
|
timestamp: Date.now(),
|
|
},
|
|
],
|
|
};
|
|
|
|
const response = await complete(llm, context, options);
|
|
|
|
expect(response).toBeDefined();
|
|
expect(response.role).toBe("assistant");
|
|
|
|
// Should handle whitespace-only gracefully
|
|
if (response.stopReason === "error") {
|
|
expect(response.errorMessage).toBeDefined();
|
|
} else {
|
|
expect(response.content).toBeDefined();
|
|
}
|
|
}
|
|
|
|
async function testEmptyAssistantMessage<TApi extends Api>(llm: Model<TApi>, options: StreamOptionsWithExtras = {}) {
|
|
// Test with empty assistant message in conversation flow
|
|
// User -> Empty Assistant -> User
|
|
const emptyAssistant: AssistantMessage = {
|
|
role: "assistant",
|
|
content: [],
|
|
api: llm.api,
|
|
provider: llm.provider,
|
|
model: llm.id,
|
|
usage: {
|
|
input: 10,
|
|
output: 0,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
totalTokens: 10,
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
},
|
|
stopReason: "stop",
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
const context: Context = {
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: "Hello, how are you?",
|
|
timestamp: Date.now(),
|
|
},
|
|
emptyAssistant,
|
|
{
|
|
role: "user",
|
|
content: "Please respond this time.",
|
|
timestamp: Date.now(),
|
|
},
|
|
],
|
|
};
|
|
|
|
const response = await complete(llm, context, options);
|
|
|
|
expect(response).toBeDefined();
|
|
expect(response.role).toBe("assistant");
|
|
|
|
// Should handle empty assistant message in context gracefully
|
|
if (response.stopReason === "error") {
|
|
expect(response.errorMessage).toBeDefined();
|
|
} else {
|
|
expect(response.content).toBeDefined();
|
|
expect(response.content.length).toBeGreaterThan(0);
|
|
}
|
|
}
|
|
|
|
describe("AI Providers Empty Message Tests", () => {
|
|
describe.skipIf(!process.env.GEMINI_API_KEY)("Google Provider Empty Messages", () => {
|
|
const llm = getModel("google", "gemini-2.5-flash");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!process.env.OPENAI_API_KEY)("OpenAI Completions Provider Empty Messages", () => {
|
|
const llm = getModel("openai", "gpt-4o-mini");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!process.env.OPENAI_API_KEY)("OpenAI Responses Provider Empty Messages", () => {
|
|
const llm = getModel("openai", "gpt-5-mini");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!hasAzureOpenAICredentials())("Azure OpenAI Responses Provider Empty Messages", () => {
|
|
const llm = getModel("azure-openai-responses", "gpt-4o-mini");
|
|
const azureDeploymentName = resolveAzureDeploymentName(llm.id);
|
|
const azureOptions = azureDeploymentName ? { azureDeploymentName } : {};
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm, azureOptions);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm, azureOptions);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm, azureOptions);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm, azureOptions);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!process.env.ANTHROPIC_API_KEY)("Anthropic Provider Empty Messages", () => {
|
|
const llm = getModel("anthropic", "claude-haiku-4-5");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!process.env.XAI_API_KEY)("xAI Provider Empty Messages", () => {
|
|
const llm = getModel("xai", "grok-4.3");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!process.env.GROQ_API_KEY)("Groq Provider Empty Messages", () => {
|
|
const llm = getModel("groq", "openai/gpt-oss-20b");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!process.env.CEREBRAS_API_KEY)("Cerebras Provider Empty Messages", () => {
|
|
const llm = getModel("cerebras", "gpt-oss-120b");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!hasCloudflareWorkersAICredentials())("Cloudflare Workers AI Provider Empty Messages", () => {
|
|
const llm = getModel("cloudflare-workers-ai", "@cf/moonshotai/kimi-k2.6");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!hasCloudflareAiGatewayCredentials())("Cloudflare AI Gateway Provider Empty Messages", () => {
|
|
const llm = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!process.env.HF_TOKEN)("Hugging Face Provider Empty Messages", () => {
|
|
const llm = getModel("huggingface", "moonshotai/Kimi-K2.5");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!process.env.TOGETHER_API_KEY)("Together AI Provider Empty Messages", () => {
|
|
const llm = getModel("together", "moonshotai/Kimi-K3");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!process.env.BASETEN_API_KEY)("Baseten Provider Empty Messages", () => {
|
|
const llm = getModel("baseten", "zai-org/GLM-5.2");
|
|
const options = { reasoningEffort: "high" } satisfies StreamOptionsWithExtras;
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm, options);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm, options);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm, options);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm, options);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider Empty Messages", () => {
|
|
const llm = getModel("zai", "glm-5.2");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!process.env.MISTRAL_API_KEY)("Mistral Provider Empty Messages", () => {
|
|
const llm = getModel("mistral", "devstral-medium-latest");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!process.env.MINIMAX_API_KEY)("MiniMax Provider Empty Messages", () => {
|
|
const llm = getModel("minimax", "MiniMax-M2.7");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!process.env.XIAOMI_API_KEY)("Xiaomi MiMo (API billing) Provider Empty Messages", () => {
|
|
const llm = getModel("xiaomi", "mimo-v2.5-pro");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!process.env.XIAOMI_TOKEN_PLAN_CN_API_KEY)(
|
|
"Xiaomi MiMo Token Plan (CN) Provider Empty Messages",
|
|
() => {
|
|
const llm = getModel("xiaomi-token-plan-cn", "mimo-v2.5-pro");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
},
|
|
);
|
|
|
|
describe.skipIf(!process.env.XIAOMI_TOKEN_PLAN_AMS_API_KEY)(
|
|
"Xiaomi MiMo Token Plan (AMS) Provider Empty Messages",
|
|
() => {
|
|
const llm = getModel("xiaomi-token-plan-ams", "mimo-v2.5-pro");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
},
|
|
);
|
|
|
|
describe.skipIf(!process.env.XIAOMI_TOKEN_PLAN_SGP_API_KEY)(
|
|
"Xiaomi MiMo Token Plan (SGP) Provider Empty Messages",
|
|
() => {
|
|
const llm = getModel("xiaomi-token-plan-sgp", "mimo-v2.5-pro");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
},
|
|
);
|
|
|
|
describe.skipIf(!process.env.QWEN_TOKEN_PLAN_API_KEY)("Qwen Token Plan Provider Empty Messages", () => {
|
|
const llm = getModel("qwen-token-plan", "qwen3.7-max");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!process.env.QWEN_TOKEN_PLAN_API_KEY)("Qwen Token Plan Individual Provider Empty Messages", () => {
|
|
const llm = getModel("qwen-token-plan-individual", "qwen3.8-max");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!process.env.QWEN_TOKEN_PLAN_CN_API_KEY)("Qwen Token Plan (CN) Provider Empty Messages", () => {
|
|
const llm = getModel("qwen-token-plan-cn", "qwen3.7-max");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!process.env.KIMI_API_KEY)("Kimi For Coding Provider Empty Messages", () => {
|
|
const llm = getModel("kimi-coding", "kimi-for-coding");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!process.env.AI_GATEWAY_API_KEY)("Vercel AI Gateway Provider Empty Messages", () => {
|
|
const llm = getModel("vercel-ai-gateway", "google/gemini-2.5-flash");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(!hasBedrockCredentials())("Amazon Bedrock Provider Empty Messages", () => {
|
|
const llm = getModel("amazon-bedrock", "global.anthropic.claude-sonnet-4-5-20250929-v1:0");
|
|
|
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm);
|
|
});
|
|
|
|
it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testWhitespaceOnlyMessage(llm);
|
|
});
|
|
|
|
it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyAssistantMessage(llm);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// OAuth-based providers (credentials from ~/.pi/agent/oauth.json)
|
|
// =========================================================================
|
|
|
|
describe("Anthropic OAuth Provider Empty Messages", () => {
|
|
const llm = getModel("anthropic", "claude-haiku-4-5");
|
|
|
|
it.skipIf(!anthropicOAuthToken)("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyMessage(llm, { apiKey: anthropicOAuthToken });
|
|
});
|
|
|
|
it.skipIf(!anthropicOAuthToken)("should handle empty string content", { retry: 3, timeout: 30000 }, async () => {
|
|
await testEmptyStringMessage(llm, { apiKey: anthropicOAuthToken });
|
|
});
|
|
|
|
it.skipIf(!anthropicOAuthToken)(
|
|
"should handle whitespace-only content",
|
|
{ retry: 3, timeout: 30000 },
|
|
async () => {
|
|
await testWhitespaceOnlyMessage(llm, { apiKey: anthropicOAuthToken });
|
|
},
|
|
);
|
|
|
|
it.skipIf(!anthropicOAuthToken)(
|
|
"should handle empty assistant message in conversation",
|
|
{ retry: 3, timeout: 30000 },
|
|
async () => {
|
|
await testEmptyAssistantMessage(llm, { apiKey: anthropicOAuthToken });
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("GitHub Copilot Provider Empty Messages", () => {
|
|
it.skipIf(!githubCopilotToken)(
|
|
"claude-haiku-4.5 - should handle empty content array",
|
|
{ retry: 3, timeout: 30000 },
|
|
async () => {
|
|
const llm = getModel("github-copilot", "claude-haiku-4.5");
|
|
await testEmptyMessage(llm, { apiKey: githubCopilotToken });
|
|
},
|
|
);
|
|
|
|
it.skipIf(!githubCopilotToken)(
|
|
"claude-haiku-4.5 - should handle empty string content",
|
|
{ retry: 3, timeout: 30000 },
|
|
async () => {
|
|
const llm = getModel("github-copilot", "claude-haiku-4.5");
|
|
await testEmptyStringMessage(llm, { apiKey: githubCopilotToken });
|
|
},
|
|
);
|
|
|
|
it.skipIf(!githubCopilotToken)(
|
|
"claude-haiku-4.5 - should handle whitespace-only content",
|
|
{ retry: 3, timeout: 30000 },
|
|
async () => {
|
|
const llm = getModel("github-copilot", "claude-haiku-4.5");
|
|
await testWhitespaceOnlyMessage(llm, { apiKey: githubCopilotToken });
|
|
},
|
|
);
|
|
|
|
it.skipIf(!githubCopilotToken)(
|
|
"claude-haiku-4.5 - should handle empty assistant message in conversation",
|
|
{ retry: 3, timeout: 30000 },
|
|
async () => {
|
|
const llm = getModel("github-copilot", "claude-haiku-4.5");
|
|
await testEmptyAssistantMessage(llm, { apiKey: githubCopilotToken });
|
|
},
|
|
);
|
|
|
|
it.skipIf(!githubCopilotToken)(
|
|
"claude-sonnet-4 - should handle empty content array",
|
|
{ retry: 3, timeout: 30000 },
|
|
async () => {
|
|
const llm = getModel("github-copilot", "claude-sonnet-4.6");
|
|
await testEmptyMessage(llm, { apiKey: githubCopilotToken });
|
|
},
|
|
);
|
|
|
|
it.skipIf(!githubCopilotToken)(
|
|
"claude-sonnet-4 - should handle empty string content",
|
|
{ retry: 3, timeout: 30000 },
|
|
async () => {
|
|
const llm = getModel("github-copilot", "claude-sonnet-4.6");
|
|
await testEmptyStringMessage(llm, { apiKey: githubCopilotToken });
|
|
},
|
|
);
|
|
|
|
it.skipIf(!githubCopilotToken)(
|
|
"claude-sonnet-4 - should handle whitespace-only content",
|
|
{ retry: 3, timeout: 30000 },
|
|
async () => {
|
|
const llm = getModel("github-copilot", "claude-sonnet-4.6");
|
|
await testWhitespaceOnlyMessage(llm, { apiKey: githubCopilotToken });
|
|
},
|
|
);
|
|
|
|
it.skipIf(!githubCopilotToken)(
|
|
"claude-sonnet-4 - should handle empty assistant message in conversation",
|
|
{ retry: 3, timeout: 30000 },
|
|
async () => {
|
|
const llm = getModel("github-copilot", "claude-sonnet-4.6");
|
|
await testEmptyAssistantMessage(llm, { apiKey: githubCopilotToken });
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("OpenAI Codex Provider Empty Messages", () => {
|
|
it.skipIf(!openaiCodexToken)(
|
|
"gpt-5.5 - should handle empty content array",
|
|
{ retry: 3, timeout: 30000 },
|
|
async () => {
|
|
const llm = getModel("openai-codex", "gpt-5.5");
|
|
await testEmptyMessage(llm, { apiKey: openaiCodexToken });
|
|
},
|
|
);
|
|
|
|
it.skipIf(!openaiCodexToken)(
|
|
"gpt-5.5 - should handle empty string content",
|
|
{ retry: 3, timeout: 30000 },
|
|
async () => {
|
|
const llm = getModel("openai-codex", "gpt-5.5");
|
|
await testEmptyStringMessage(llm, { apiKey: openaiCodexToken });
|
|
},
|
|
);
|
|
|
|
it.skipIf(!openaiCodexToken)(
|
|
"gpt-5.5 - should handle whitespace-only content",
|
|
{ retry: 3, timeout: 30000 },
|
|
async () => {
|
|
const llm = getModel("openai-codex", "gpt-5.5");
|
|
await testWhitespaceOnlyMessage(llm, { apiKey: openaiCodexToken });
|
|
},
|
|
);
|
|
|
|
it.skipIf(!openaiCodexToken)(
|
|
"gpt-5.5 - should handle empty assistant message in conversation",
|
|
{ retry: 3, timeout: 30000 },
|
|
async () => {
|
|
const llm = getModel("openai-codex", "gpt-5.5");
|
|
await testEmptyAssistantMessage(llm, { apiKey: openaiCodexToken });
|
|
},
|
|
);
|
|
});
|
|
});
|