mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
* feat(ai): support Azure Foundry Chat Completions deployments The Azure provider only implemented the Responses API, so Foundry deployments that speak Chat Completions could not be used. On Chat Completions the deepseek/deepseek-v4-pro model sends `thinking` and `prompt_cache_key`, both of which Azure rejects with 400. The provider now dispatches openai-completions alongside azure-openai-responses. A stream wrapper resolves the Azure endpoint onto the model first, since the shared openai-completions implementation reads it off the model. The request-shape differences are catalog compat rather than api code: reasoning_effort instead of DeepSeek's thinking field, no prompt cache parameters, reasoning_content kept on assistant turns so the prefix Foundry cached stays byte-identical, and effort clamped to the low/medium/high the deployment accepts. The provider is renamed from azure-openai-responses to azure, since it now serves more than the Responses API. The azure-openai-responses api id is unchanged. Breaking: auth.json and models.json entries keyed on azure-openai-responses must move to azure, and existing sessions see a different provider on their next turn. closes #9645 * feat(ai): apply the Azure deployment name map to Chat Completions The Responses path already sent the deployment name from AZURE_OPENAI_DEPLOYMENT_NAME_MAP. The Chat Completions path sent the catalog model id, so any deployment not named after the model 404'd. The Azure provider now swaps the deployment name into the request via onPayload before dispatch, then runs the caller's own onPayload on it. model.id stays the catalog id. The map helpers move to azure-openai-config.ts so the provider can share them without importing the SDK. * docs(ai,coding-agent): add changelog entries for the Azure provider rename Breaking: the azure-openai-responses provider is now azure. Users rename the key in auth.json, models.json, and settings.json; SDK users switch to getModel("azure", ...) and providers/azure. Also note Azure Foundry Chat Completions support (DeepSeek V4 Pro). --------- Co-authored-by: David Brailovsky <davidbrai@gmail.com>
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", "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 });
|
|
},
|
|
);
|
|
});
|
|
});
|