feat(ai): add Together AI provider
This commit is contained in:
@@ -178,6 +178,18 @@ describe("AI Providers Abort Tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.TOGETHER_API_KEY)("Together AI Provider Abort", () => {
|
||||
const llm = getModel("together", "moonshotai/Kimi-K2.6");
|
||||
|
||||
it("should abort mid-stream", { retry: 3 }, async () => {
|
||||
await testAbortSignal(llm, { reasoningEffort: "high" });
|
||||
});
|
||||
|
||||
it("should handle immediate abort", { retry: 3 }, async () => {
|
||||
await testImmediateAbort(llm, { reasoningEffort: "high" });
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.MINIMAX_API_KEY)("MiniMax Provider Abort", () => {
|
||||
const llm = getModel("minimax", "MiniMax-M2.7");
|
||||
|
||||
|
||||
@@ -325,6 +325,22 @@ describe("Context overflow error handling", () => {
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Together AI
|
||||
// Uses OpenAI-compatible Chat Completions API
|
||||
// =============================================================================
|
||||
|
||||
describe.skipIf(!process.env.TOGETHER_API_KEY)("Together AI", () => {
|
||||
it("Kimi-K2.6 - should detect overflow via isContextOverflow", async () => {
|
||||
const model = getModel("together", "moonshotai/Kimi-K2.6");
|
||||
const result = await testContextOverflow(model, process.env.TOGETHER_API_KEY!);
|
||||
logResult(result);
|
||||
|
||||
expect(result.stopReason).toBe("error");
|
||||
expect(isContextOverflow(result.response, model.contextWindow)).toBe(true);
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// z.ai
|
||||
// Special case: may return explicit overflow error text, may accept overflow silently,
|
||||
|
||||
@@ -107,6 +107,8 @@ const PROVIDER_MODEL_PAIRS: ProviderModelPair[] = [
|
||||
{ provider: "groq", model: "openai/gpt-oss-120b", label: "groq-gpt-oss-120b" },
|
||||
// Hugging Face
|
||||
{ provider: "huggingface", model: "moonshotai/Kimi-K2.5", label: "huggingface-kimi-k2.5" },
|
||||
// Together AI
|
||||
{ provider: "together", model: "moonshotai/Kimi-K2.6", label: "together-kimi-k2.6" },
|
||||
// Kimi For Coding
|
||||
{ provider: "kimi-coding", model: "kimi-k2-thinking", label: "kimi-coding-k2-thinking" },
|
||||
// Mistral
|
||||
|
||||
@@ -367,6 +367,26 @@ describe("AI Providers Empty Message Tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.TOGETHER_API_KEY)("Together AI Provider Empty Messages", () => {
|
||||
const llm = getModel("together", "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.ZAI_API_KEY)("zAI Provider Empty Messages", () => {
|
||||
const llm = getModel("zai", "glm-4.5-air");
|
||||
|
||||
|
||||
@@ -298,6 +298,19 @@ describe("Tool Results with Images", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.TOGETHER_API_KEY)("Together AI Provider (Kimi-K2.6)", () => {
|
||||
const llm = getModel("together", "moonshotai/Kimi-K2.6");
|
||||
const options = { reasoningEffort: "high" } satisfies StreamOptionsWithExtras;
|
||||
|
||||
it("should handle tool result with only image", { retry: 3, timeout: 30000 }, async () => {
|
||||
await handleToolWithImageResult(llm, options);
|
||||
});
|
||||
|
||||
it("should handle tool result with text and image", { retry: 3, timeout: 30000 }, async () => {
|
||||
await handleToolWithTextAndImageResult(llm, options);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.XIAOMI_API_KEY)("Xiaomi MiMo (API billing) Provider (mimo-v2.5-pro)", () => {
|
||||
const llm = getModel("xiaomi", "mimo-v2.5-pro");
|
||||
|
||||
|
||||
@@ -35,6 +35,13 @@ describe("isContextOverflow", () => {
|
||||
expect(isContextOverflow(message, 32768)).toBe(true);
|
||||
});
|
||||
|
||||
it("detects Together AI context length errors", () => {
|
||||
const message = createErrorMessage(
|
||||
"400 The input (516368 tokens) is longer than the model's context length (262144 tokens).",
|
||||
);
|
||||
expect(isContextOverflow(message, 262144)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not treat generic non-overflow Ollama errors as overflow", () => {
|
||||
const message = createErrorMessage("500 `model runner crashed unexpectedly`");
|
||||
expect(isContextOverflow(message, 32768)).toBe(false);
|
||||
|
||||
@@ -759,6 +759,34 @@ describe("Generate E2E Tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.TOGETHER_API_KEY)("Together AI Provider (Kimi-K2.6 via OpenAI Completions)", () => {
|
||||
const llm = getModel("together", "moonshotai/Kimi-K2.6");
|
||||
|
||||
it("should complete basic text generation", { retry: 3 }, async () => {
|
||||
await basicTextGeneration(llm);
|
||||
});
|
||||
|
||||
it("should handle tool calling", { retry: 3 }, async () => {
|
||||
await handleToolCall(llm);
|
||||
});
|
||||
|
||||
it("should handle streaming", { retry: 3 }, async () => {
|
||||
await handleStreaming(llm);
|
||||
});
|
||||
|
||||
it("should handle thinking mode", { retry: 3 }, async () => {
|
||||
await handleThinking(llm, { reasoningEffort: "high" });
|
||||
});
|
||||
|
||||
it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => {
|
||||
await multiTurn(llm, { reasoningEffort: "high" });
|
||||
});
|
||||
|
||||
it("should handle image input", { retry: 3 }, async () => {
|
||||
await handleImage(llm);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.OPENROUTER_API_KEY)("OpenRouter Provider (glm-4.5v via OpenAI Completions)", () => {
|
||||
const llm = getModel("openrouter", "z-ai/glm-4.5v");
|
||||
|
||||
|
||||
78
packages/ai/test/together-models.test.ts
Normal file
78
packages/ai/test/together-models.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.js";
|
||||
import { getModel } from "../src/models.js";
|
||||
|
||||
const originalTogetherApiKey = process.env.TOGETHER_API_KEY;
|
||||
|
||||
afterEach(() => {
|
||||
if (originalTogetherApiKey === undefined) {
|
||||
delete process.env.TOGETHER_API_KEY;
|
||||
} else {
|
||||
process.env.TOGETHER_API_KEY = originalTogetherApiKey;
|
||||
}
|
||||
});
|
||||
|
||||
describe("Together models", () => {
|
||||
it("registers the default Kimi K2.6 model via OpenAI-compatible Chat Completions API", () => {
|
||||
const model = getModel("together", "moonshotai/Kimi-K2.6");
|
||||
|
||||
expect(model).toBeDefined();
|
||||
expect(model.api).toBe("openai-completions");
|
||||
expect(model.provider).toBe("together");
|
||||
expect(model.baseUrl).toBe("https://api.together.ai/v1");
|
||||
expect(model.reasoning).toBe(true);
|
||||
expect(model.thinkingLevelMap).toEqual({ minimal: null, low: null, medium: null });
|
||||
expect(model.input).toEqual(["text", "image"]);
|
||||
expect(model.contextWindow).toBe(262144);
|
||||
expect(model.maxTokens).toBe(131000);
|
||||
expect(model.cost).toEqual({
|
||||
input: 1.2,
|
||||
output: 4.5,
|
||||
cacheRead: 0.2,
|
||||
cacheWrite: 0,
|
||||
});
|
||||
expect(model.compat).toEqual({
|
||||
supportsStore: false,
|
||||
supportsDeveloperRole: false,
|
||||
supportsReasoningEffort: false,
|
||||
maxTokensField: "max_tokens",
|
||||
thinkingFormat: "together",
|
||||
supportsStrictMode: false,
|
||||
supportsLongCacheRetention: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("models Together reasoning controls from the Together API surface", () => {
|
||||
const gptOss = getModel("together", "openai/gpt-oss-120b");
|
||||
expect(gptOss.thinkingLevelMap).toEqual({ off: null, minimal: null });
|
||||
expect(gptOss.compat).toMatchObject({
|
||||
supportsReasoningEffort: true,
|
||||
thinkingFormat: "openai",
|
||||
});
|
||||
|
||||
const deepSeekV4 = getModel("together", "deepseek-ai/DeepSeek-V4-Pro");
|
||||
expect(deepSeekV4.thinkingLevelMap).toEqual({
|
||||
minimal: null,
|
||||
low: null,
|
||||
medium: null,
|
||||
high: "high",
|
||||
xhigh: null,
|
||||
});
|
||||
expect(deepSeekV4.compat).toMatchObject({
|
||||
supportsReasoningEffort: true,
|
||||
thinkingFormat: "together",
|
||||
});
|
||||
|
||||
const minimax = getModel("together", "MiniMaxAI/MiniMax-M2.7");
|
||||
expect(minimax.thinkingLevelMap).toEqual({ off: null, minimal: null, low: null, medium: null });
|
||||
expect(minimax.compat?.thinkingFormat).toBeUndefined();
|
||||
expect(minimax.compat?.supportsReasoningEffort).toBe(false);
|
||||
});
|
||||
|
||||
it("resolves TOGETHER_API_KEY from the environment", () => {
|
||||
process.env.TOGETHER_API_KEY = "test-together-key";
|
||||
|
||||
expect(findEnvKeys("together")).toEqual(["TOGETHER_API_KEY"]);
|
||||
expect(getEnvApiKey("together")).toBe("test-together-key");
|
||||
});
|
||||
});
|
||||
@@ -180,6 +180,14 @@ describe("Token Statistics on Abort", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.TOGETHER_API_KEY)("Together AI Provider", () => {
|
||||
const llm = getModel("together", "moonshotai/Kimi-K2.6");
|
||||
|
||||
it("should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => {
|
||||
await testTokensOnAbort(llm);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider", () => {
|
||||
const llm = getModel("zai", "glm-4.5-air");
|
||||
|
||||
|
||||
@@ -191,6 +191,14 @@ describe("Tool Call Without Result Tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.TOGETHER_API_KEY)("Together AI Provider", () => {
|
||||
const model = getModel("together", "moonshotai/Kimi-K2.6");
|
||||
|
||||
it("should filter out tool calls without corresponding tool results", { retry: 3, timeout: 30000 }, async () => {
|
||||
await testToolCallWithoutResult(model, { reasoningEffort: "high" });
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider", () => {
|
||||
const model = getModel("zai", "glm-4.5-air");
|
||||
|
||||
|
||||
@@ -374,6 +374,28 @@ describe("totalTokens field", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Together AI
|
||||
// =========================================================================
|
||||
|
||||
describe.skipIf(!process.env.TOGETHER_API_KEY)("Together AI", () => {
|
||||
it("Kimi-K2.6 - should return totalTokens equal to sum of components", { retry: 3, timeout: 60000 }, async () => {
|
||||
const llm = getModel("together", "moonshotai/Kimi-K2.6");
|
||||
|
||||
console.log(`\nTogether AI / ${llm.id}:`);
|
||||
const { first, second } = await testTotalTokensWithCache(llm, {
|
||||
apiKey: process.env.TOGETHER_API_KEY,
|
||||
reasoningEffort: "high",
|
||||
});
|
||||
|
||||
logUsage("First request", first);
|
||||
logUsage("Second request", second);
|
||||
|
||||
assertTotalTokensEqualsComponents(first);
|
||||
assertTotalTokensEqualsComponents(second);
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// z.ai
|
||||
// =========================================================================
|
||||
|
||||
@@ -546,6 +546,23 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.TOGETHER_API_KEY)("Together AI Provider Unicode Handling", () => {
|
||||
const llm = getModel("together", "moonshotai/Kimi-K2.6");
|
||||
const options = { reasoningEffort: "high" } satisfies StreamOptionsWithExtras;
|
||||
|
||||
it("should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => {
|
||||
await testEmojiInToolResults(llm, options);
|
||||
});
|
||||
|
||||
it("should handle real-world LinkedIn comment data with emoji", { retry: 3, timeout: 30000 }, async () => {
|
||||
await testRealWorldLinkedInData(llm, options);
|
||||
});
|
||||
|
||||
it("should handle unpaired high surrogate (0xD83D) in tool results", { retry: 3, timeout: 30000 }, async () => {
|
||||
await testUnpairedHighSurrogate(llm, options);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider Unicode Handling", () => {
|
||||
const llm = getModel("zai", "glm-4.5-air");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user