feat(ai): add Together AI provider
This commit is contained in:
@@ -156,7 +156,7 @@ Create provider file exporting:
|
||||
### 5. Tests (`packages/ai/test/`)
|
||||
|
||||
- Always add the provider to `stream.test.ts` with at least one representative model, even if it reuses an existing API implementation such as `openai-completions`.
|
||||
- Add the provider to the broader provider matrix where applicable: `tokens.test.ts`, `abort.test.ts`, `empty.test.ts`, `context-overflow.test.ts`, `image-limits.test.ts`, `unicode-surrogate.test.ts`, `tool-call-without-result.test.ts`, `image-tool-result.test.ts`, `total-tokens.test.ts`, `cross-provider-handoff.test.ts`.
|
||||
- Add the provider to the broader provider matrix where applicable: `tokens.test.ts`, `abort.test.ts`, `empty.test.ts`, `context-overflow.test.ts`, `unicode-surrogate.test.ts`, `tool-call-without-result.test.ts`, `image-tool-result.test.ts`, `total-tokens.test.ts`, `cross-provider-handoff.test.ts`.
|
||||
- For `cross-provider-handoff.test.ts`, add at least one provider/model pair. If the provider exposes multiple model families (for example GPT and Claude), add at least one pair per family.
|
||||
- For non-standard auth, create utility (e.g., `bedrock-utils.ts`) with credential detection.
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added Together AI as a built-in OpenAI-compatible provider with generated model metadata and `TOGETHER_API_KEY` authentication ([#3624](https://github.com/earendil-works/pi-mono/pull/3624) by [@Nutlope](https://github.com/Nutlope)).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed OpenAI Responses requests for models that support disabling reasoning to send `reasoning.effort: "none"` when thinking is off.
|
||||
|
||||
@@ -66,6 +66,7 @@ Unified LLM API with automatic model discovery, provider configuration, token an
|
||||
- **OpenRouter**
|
||||
- **Vercel AI Gateway**
|
||||
- **MiniMax**
|
||||
- **Together AI**
|
||||
- **GitHub Copilot** (requires OAuth, see below)
|
||||
- **Amazon Bedrock**
|
||||
- **OpenCode Zen**
|
||||
@@ -800,7 +801,7 @@ A **provider** offers models through a specific API. For example:
|
||||
- **Google** models use the `google-generative-ai` API
|
||||
- **OpenAI** models use the `openai-responses` API
|
||||
- **Mistral** models use the `mistral-conversations` API
|
||||
- **xAI, Cerebras, Groq, etc.** models use the `openai-completions` API (OpenAI-compatible)
|
||||
- **xAI, Cerebras, Groq, Together AI, etc.** models use the `openai-completions` API (OpenAI-compatible)
|
||||
|
||||
### Querying Providers and Models
|
||||
|
||||
@@ -922,7 +923,7 @@ const ollamaReasoningModel: Model<'openai-completions'> = {
|
||||
|
||||
### OpenAI Compatibility Settings
|
||||
|
||||
The `openai-completions` API is implemented by many providers with minor differences. By default, the library auto-detects compatibility settings based on `baseUrl` for a small set of known OpenAI-compatible providers (Cerebras, xAI, Chutes, DeepSeek, zAi, OpenCode, Cloudflare Workers AI, etc.). For custom proxies or unknown endpoints, you can override these settings via the `compat` field. For `openai-responses` models, the compat field only supports Responses-specific flags.
|
||||
The `openai-completions` API is implemented by many providers with minor differences. By default, the library auto-detects compatibility settings based on `baseUrl` for a small set of known OpenAI-compatible providers (Cerebras, xAI, Chutes, DeepSeek, Together AI, zAi, OpenCode, Cloudflare Workers AI, etc.). For custom proxies or unknown endpoints, you can override these settings via the `compat` field. For `openai-responses` models, the compat field only supports Responses-specific flags.
|
||||
|
||||
```typescript
|
||||
interface OpenAICompletionsCompat {
|
||||
@@ -937,7 +938,7 @@ interface OpenAICompletionsCompat {
|
||||
requiresAssistantAfterToolResult?: boolean; // Whether tool results must be followed by an assistant message (default: false)
|
||||
requiresThinkingAsText?: boolean; // Whether thinking blocks must be converted to text (default: false)
|
||||
requiresReasoningContentOnAssistantMessages?: boolean; // Whether all replayed assistant messages must include empty reasoning_content when reasoning is enabled (default: auto-detected for DeepSeek)
|
||||
thinkingFormat?: 'openai' | 'deepseek' | 'zai' | 'qwen' | 'qwen-chat-template'; // Format for reasoning param: 'openai' uses reasoning_effort, 'deepseek' uses thinking: { type } plus reasoning_effort, 'zai' uses enable_thinking, 'qwen' uses enable_thinking, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking (default: openai)
|
||||
thinkingFormat?: 'openai' | 'openrouter' | 'deepseek' | 'together' | 'zai' | 'qwen' | 'qwen-chat-template'; // Format for reasoning param: 'openai' uses reasoning_effort, 'openrouter' uses reasoning: { effort }, 'deepseek' uses thinking: { type } plus reasoning_effort, 'together' uses reasoning: { enabled } plus reasoning_effort when supported, 'zai' uses enable_thinking, 'qwen' uses enable_thinking, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking (default: openai)
|
||||
cacheControlFormat?: 'anthropic'; // Anthropic-style cache_control on system prompt, last tool, and last user/assistant text content
|
||||
openRouterRouting?: OpenRouterRouting; // OpenRouter routing preferences (default: {})
|
||||
vercelGatewayRouting?: VercelGatewayRouting; // Vercel AI Gateway routing preferences (default: {})
|
||||
@@ -1110,6 +1111,7 @@ In Node.js environments, you can set environment variables to avoid passing API
|
||||
| Cloudflare Workers AI | `CLOUDFLARE_API_KEY` + `CLOUDFLARE_ACCOUNT_ID` |
|
||||
| xAI | `XAI_API_KEY` |
|
||||
| Fireworks | `FIREWORKS_API_KEY` |
|
||||
| Together AI | `TOGETHER_API_KEY` |
|
||||
| OpenRouter | `OPENROUTER_API_KEY` |
|
||||
| Vercel AI Gateway | `AI_GATEWAY_API_KEY` |
|
||||
| zAI | `ZAI_API_KEY` |
|
||||
|
||||
@@ -69,6 +69,58 @@ const KIMI_STATIC_HEADERS = {
|
||||
"User-Agent": "KimiCLI/1.5",
|
||||
} as const;
|
||||
|
||||
const TOGETHER_BASE_URL = "https://api.together.ai/v1";
|
||||
const TOGETHER_BASE_COMPAT: OpenAICompletionsCompat = {
|
||||
supportsStore: false,
|
||||
supportsDeveloperRole: false,
|
||||
supportsReasoningEffort: false,
|
||||
maxTokensField: "max_tokens",
|
||||
supportsStrictMode: false,
|
||||
supportsLongCacheRetention: false,
|
||||
};
|
||||
const TOGETHER_TOGGLE_REASONING_COMPAT: OpenAICompletionsCompat = {
|
||||
...TOGETHER_BASE_COMPAT,
|
||||
thinkingFormat: "together",
|
||||
};
|
||||
const TOGETHER_REASONING_EFFORT_COMPAT: OpenAICompletionsCompat = {
|
||||
...TOGETHER_BASE_COMPAT,
|
||||
supportsReasoningEffort: true,
|
||||
thinkingFormat: "openai",
|
||||
};
|
||||
const TOGETHER_TOGGLE_REASONING_EFFORT_COMPAT: OpenAICompletionsCompat = {
|
||||
...TOGETHER_TOGGLE_REASONING_COMPAT,
|
||||
supportsReasoningEffort: true,
|
||||
};
|
||||
const TOGETHER_REASONING_ONLY_MODELS = new Set([
|
||||
"deepseek-ai/DeepSeek-R1",
|
||||
"MiniMaxAI/MiniMax-M2.5",
|
||||
"MiniMaxAI/MiniMax-M2.7",
|
||||
]);
|
||||
const TOGETHER_REASONING_EFFORT_MODELS = new Set(["openai/gpt-oss-20b", "openai/gpt-oss-120b"]);
|
||||
const TOGETHER_TOGGLE_REASONING_EFFORT_MODELS = new Set(["deepseek-ai/DeepSeek-V4-Pro"]);
|
||||
const TOGETHER_FIXED_REASONING_LEVEL_MAP = {
|
||||
off: null,
|
||||
minimal: null,
|
||||
low: null,
|
||||
medium: null,
|
||||
} as const;
|
||||
const TOGETHER_REASONING_EFFORT_LEVEL_MAP = {
|
||||
off: null,
|
||||
minimal: null,
|
||||
} as const;
|
||||
const TOGETHER_DEEPSEEK_V4_THINKING_LEVEL_MAP = {
|
||||
minimal: null,
|
||||
low: null,
|
||||
medium: null,
|
||||
high: "high",
|
||||
xhigh: null,
|
||||
} as const;
|
||||
const TOGETHER_TOGGLE_REASONING_LEVEL_MAP = {
|
||||
minimal: null,
|
||||
low: null,
|
||||
medium: null,
|
||||
} as const;
|
||||
|
||||
const AI_GATEWAY_MODELS_URL = "https://ai-gateway.vercel.sh/v1";
|
||||
const AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh";
|
||||
const ZAI_TOOL_STREAM_UNSUPPORTED_MODELS = new Set(["glm-4.5", "glm-4.5-air", "glm-4.5-flash", "glm-4.5v"]);
|
||||
@@ -100,6 +152,25 @@ function mergeThinkingLevelMap(model: Model<any>, map: NonNullable<Model<any>["t
|
||||
model.thinkingLevelMap = { ...model.thinkingLevelMap, ...map };
|
||||
}
|
||||
|
||||
function getTogetherCompat(modelId: string, reasoning: boolean): OpenAICompletionsCompat {
|
||||
if (!reasoning) return TOGETHER_BASE_COMPAT;
|
||||
if (TOGETHER_REASONING_EFFORT_MODELS.has(modelId)) return TOGETHER_REASONING_EFFORT_COMPAT;
|
||||
if (TOGETHER_TOGGLE_REASONING_EFFORT_MODELS.has(modelId)) return TOGETHER_TOGGLE_REASONING_EFFORT_COMPAT;
|
||||
if (TOGETHER_REASONING_ONLY_MODELS.has(modelId)) return TOGETHER_BASE_COMPAT;
|
||||
return TOGETHER_TOGGLE_REASONING_COMPAT;
|
||||
}
|
||||
|
||||
function getTogetherThinkingLevelMap(
|
||||
modelId: string,
|
||||
reasoning: boolean,
|
||||
): NonNullable<Model<any>["thinkingLevelMap"]> | undefined {
|
||||
if (!reasoning) return undefined;
|
||||
if (TOGETHER_REASONING_EFFORT_MODELS.has(modelId)) return { ...TOGETHER_REASONING_EFFORT_LEVEL_MAP };
|
||||
if (TOGETHER_TOGGLE_REASONING_EFFORT_MODELS.has(modelId)) return { ...TOGETHER_DEEPSEEK_V4_THINKING_LEVEL_MAP };
|
||||
if (TOGETHER_REASONING_ONLY_MODELS.has(modelId)) return { ...TOGETHER_FIXED_REASONING_LEVEL_MAP };
|
||||
return { ...TOGETHER_TOGGLE_REASONING_LEVEL_MAP };
|
||||
}
|
||||
|
||||
function supportsOpenAiXhigh(modelId: string): boolean {
|
||||
return (
|
||||
modelId.includes("gpt-5.2") ||
|
||||
@@ -697,6 +768,38 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
||||
}
|
||||
}
|
||||
|
||||
// Process Together AI models
|
||||
const togetherProvider = data.together ?? data.togetherai ?? data["together-ai"];
|
||||
if (togetherProvider?.models) {
|
||||
for (const [modelId, model] of Object.entries(togetherProvider.models)) {
|
||||
const m = model as ModelsDevModel & { status?: string };
|
||||
if (m.tool_call !== true) continue;
|
||||
if (m.status === "deprecated") continue;
|
||||
|
||||
const reasoning = m.reasoning === true;
|
||||
const thinkingLevelMap = getTogetherThinkingLevelMap(modelId, reasoning);
|
||||
models.push({
|
||||
id: modelId,
|
||||
name: m.name || modelId,
|
||||
api: "openai-completions",
|
||||
provider: "together",
|
||||
baseUrl: TOGETHER_BASE_URL,
|
||||
reasoning,
|
||||
...(thinkingLevelMap ? { thinkingLevelMap } : {}),
|
||||
input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
|
||||
cost: {
|
||||
input: m.cost?.input || 0,
|
||||
output: m.cost?.output || 0,
|
||||
cacheRead: m.cost?.cache_read || 0,
|
||||
cacheWrite: m.cost?.cache_write || 0,
|
||||
},
|
||||
compat: getTogetherCompat(modelId, reasoning),
|
||||
contextWindow: m.limit?.context || 4096,
|
||||
maxTokens: m.limit?.output || 4096,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process OpenCode models (Zen and Go)
|
||||
// API mapping based on provider.npm field:
|
||||
// - @ai-sdk/openai → openai-responses
|
||||
|
||||
@@ -117,6 +117,7 @@ function getApiKeyEnvVars(provider: string): readonly string[] | undefined {
|
||||
"moonshotai-cn": "MOONSHOT_API_KEY",
|
||||
huggingface: "HF_TOKEN",
|
||||
fireworks: "FIREWORKS_API_KEY",
|
||||
together: "TOGETHER_API_KEY",
|
||||
opencode: "OPENCODE_API_KEY",
|
||||
"opencode-go": "OPENCODE_API_KEY",
|
||||
"kimi-coding": "KIMI_API_KEY",
|
||||
|
||||
@@ -10188,23 +10188,6 @@ export const MODELS = {
|
||||
contextWindow: 65536,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"mistralai/mixtral-8x7b-instruct": {
|
||||
id: "mistralai/mixtral-8x7b-instruct",
|
||||
name: "Mistral: Mixtral 8x7B Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.54,
|
||||
output: 0.54,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 32768,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"mistralai/pixtral-large-2411": {
|
||||
id: "mistralai/pixtral-large-2411",
|
||||
name: "Mistral: Pixtral Large 2411",
|
||||
@@ -10341,23 +10324,6 @@ export const MODELS = {
|
||||
contextWindow: 131072,
|
||||
maxTokens: 163840,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"nvidia/llama-3.1-nemotron-70b-instruct": {
|
||||
id: "nvidia/llama-3.1-nemotron-70b-instruct",
|
||||
name: "NVIDIA: Llama 3.1 Nemotron 70B Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 1.2,
|
||||
output: 1.2,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"nvidia/llama-3.3-nemotron-super-49b-v1.5": {
|
||||
id: "nvidia/llama-3.3-nemotron-super-49b-v1.5",
|
||||
name: "NVIDIA: Llama 3.3 Nemotron Super 49B V1.5",
|
||||
@@ -12963,13 +12929,13 @@ export const MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.38,
|
||||
output: 1.74,
|
||||
cacheRead: 0,
|
||||
input: 0.39999999999999997,
|
||||
output: 1.75,
|
||||
cacheRead: 0.08,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 202752,
|
||||
maxTokens: 4096,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"z-ai/glm-4.7-flash": {
|
||||
id: "z-ai/glm-4.7-flash",
|
||||
@@ -13193,6 +13159,328 @@ export const MODELS = {
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
},
|
||||
"together": {
|
||||
"MiniMaxAI/MiniMax-M2.5": {
|
||||
id: "MiniMaxAI/MiniMax-M2.5",
|
||||
name: "MiniMax-M2.5",
|
||||
api: "openai-completions",
|
||||
provider: "together",
|
||||
baseUrl: "https://api.together.ai/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null},
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.3,
|
||||
output: 1.2,
|
||||
cacheRead: 0.06,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 204800,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"MiniMaxAI/MiniMax-M2.7": {
|
||||
id: "MiniMaxAI/MiniMax-M2.7",
|
||||
name: "MiniMax-M2.7",
|
||||
api: "openai-completions",
|
||||
provider: "together",
|
||||
baseUrl: "https://api.together.ai/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null},
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.3,
|
||||
output: 1.2,
|
||||
cacheRead: 0.06,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 202752,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"Qwen/Qwen3-235B-A22B-Instruct-2507-tput": {
|
||||
id: "Qwen/Qwen3-235B-A22B-Instruct-2507-tput",
|
||||
name: "Qwen3 235B A22B Instruct 2507 FP8",
|
||||
api: "openai-completions",
|
||||
provider: "together",
|
||||
baseUrl: "https://api.together.ai/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"minimal":null,"low":null,"medium":null},
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.2,
|
||||
output: 0.6,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 262144,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": {
|
||||
id: "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8",
|
||||
name: "Qwen3 Coder 480B A35B Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "together",
|
||||
baseUrl: "https://api.together.ai/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 2,
|
||||
output: 2,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 262144,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"Qwen/Qwen3-Coder-Next-FP8": {
|
||||
id: "Qwen/Qwen3-Coder-Next-FP8",
|
||||
name: "Qwen3 Coder Next FP8",
|
||||
api: "openai-completions",
|
||||
provider: "together",
|
||||
baseUrl: "https://api.together.ai/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"minimal":null,"low":null,"medium":null},
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.5,
|
||||
output: 1.2,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 262144,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"Qwen/Qwen3.5-397B-A17B": {
|
||||
id: "Qwen/Qwen3.5-397B-A17B",
|
||||
name: "Qwen3.5 397B A17B",
|
||||
api: "openai-completions",
|
||||
provider: "together",
|
||||
baseUrl: "https://api.together.ai/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"minimal":null,"low":null,"medium":null},
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.6,
|
||||
output: 3.6,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 130000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"Qwen/Qwen3.6-Plus": {
|
||||
id: "Qwen/Qwen3.6-Plus",
|
||||
name: "Qwen3.6 Plus",
|
||||
api: "openai-completions",
|
||||
provider: "together",
|
||||
baseUrl: "https://api.together.ai/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"minimal":null,"low":null,"medium":null},
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.5,
|
||||
output: 3,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 500000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"deepseek-ai/DeepSeek-V3": {
|
||||
id: "deepseek-ai/DeepSeek-V3",
|
||||
name: "DeepSeek V3",
|
||||
api: "openai-completions",
|
||||
provider: "together",
|
||||
baseUrl: "https://api.together.ai/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"minimal":null,"low":null,"medium":null},
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 1.25,
|
||||
output: 1.25,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"deepseek-ai/DeepSeek-V3-1": {
|
||||
id: "deepseek-ai/DeepSeek-V3-1",
|
||||
name: "DeepSeek V3.1",
|
||||
api: "openai-completions",
|
||||
provider: "together",
|
||||
baseUrl: "https://api.together.ai/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"minimal":null,"low":null,"medium":null},
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.6,
|
||||
output: 1.7,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"deepseek-ai/DeepSeek-V4-Pro": {
|
||||
id: "deepseek-ai/DeepSeek-V4-Pro",
|
||||
name: "DeepSeek V4 Pro",
|
||||
api: "openai-completions",
|
||||
provider: "together",
|
||||
baseUrl: "https://api.together.ai/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":null},
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 2.1,
|
||||
output: 4.4,
|
||||
cacheRead: 0.2,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 512000,
|
||||
maxTokens: 384000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"essentialai/Rnj-1-Instruct": {
|
||||
id: "essentialai/Rnj-1-Instruct",
|
||||
name: "Rnj-1 Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "together",
|
||||
baseUrl: "https://api.together.ai/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.15,
|
||||
output: 0.15,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 32768,
|
||||
maxTokens: 32768,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"google/gemma-4-31B-it": {
|
||||
id: "google/gemma-4-31B-it",
|
||||
name: "Gemma 4 31B Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "together",
|
||||
baseUrl: "https://api.together.ai/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"minimal":null,"low":null,"medium":null},
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.2,
|
||||
output: 0.5,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meta-llama/Llama-3.3-70B-Instruct-Turbo": {
|
||||
id: "meta-llama/Llama-3.3-70B-Instruct-Turbo",
|
||||
name: "Llama 3.3 70B",
|
||||
api: "openai-completions",
|
||||
provider: "together",
|
||||
baseUrl: "https://api.together.ai/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.88,
|
||||
output: 0.88,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"moonshotai/Kimi-K2.5": {
|
||||
id: "moonshotai/Kimi-K2.5",
|
||||
name: "Kimi K2.5",
|
||||
api: "openai-completions",
|
||||
provider: "together",
|
||||
baseUrl: "https://api.together.ai/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"minimal":null,"low":null,"medium":null},
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.5,
|
||||
output: 2.8,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 262144,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"moonshotai/Kimi-K2.6": {
|
||||
id: "moonshotai/Kimi-K2.6",
|
||||
name: "Kimi K2.6",
|
||||
api: "openai-completions",
|
||||
provider: "together",
|
||||
baseUrl: "https://api.together.ai/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"minimal":null,"low":null,"medium":null},
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 1.2,
|
||||
output: 4.5,
|
||||
cacheRead: 0.2,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 131000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"openai/gpt-oss-120b": {
|
||||
id: "openai/gpt-oss-120b",
|
||||
name: "GPT OSS 120B",
|
||||
api: "openai-completions",
|
||||
provider: "together",
|
||||
baseUrl: "https://api.together.ai/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"openai"},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"off":null,"minimal":null},
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.15,
|
||||
output: 0.6,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"zai-org/GLM-5.1": {
|
||||
id: "zai-org/GLM-5.1",
|
||||
name: "GLM-5.1",
|
||||
api: "openai-completions",
|
||||
provider: "together",
|
||||
baseUrl: "https://api.together.ai/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"minimal":null,"low":null,"medium":null},
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 1.4,
|
||||
output: 4.4,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 202752,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
},
|
||||
"vercel-ai-gateway": {
|
||||
"alibaba/qwen-3-14b": {
|
||||
id: "alibaba/qwen-3-14b",
|
||||
|
||||
@@ -575,6 +575,15 @@ function buildParams(
|
||||
} else if (model.thinkingLevelMap?.off !== null) {
|
||||
openRouterParams.reasoning = { effort: model.thinkingLevelMap?.off ?? "none" };
|
||||
}
|
||||
} else if (compat.thinkingFormat === "together" && model.reasoning) {
|
||||
const togetherParams = params as Omit<typeof params, "reasoning_effort"> & {
|
||||
reasoning?: { enabled: boolean };
|
||||
reasoning_effort?: string;
|
||||
};
|
||||
togetherParams.reasoning = { enabled: !!options?.reasoningEffort };
|
||||
if (options?.reasoningEffort && compat.supportsReasoningEffort) {
|
||||
togetherParams.reasoning_effort = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
|
||||
}
|
||||
} else if (options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) {
|
||||
// OpenAI-style reasoning_effort
|
||||
(params as any).reasoning_effort = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
|
||||
@@ -1050,6 +1059,8 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
|
||||
const baseUrl = model.baseUrl;
|
||||
|
||||
const isZai = provider === "zai" || baseUrl.includes("api.z.ai");
|
||||
const isTogether =
|
||||
provider === "together" || baseUrl.includes("api.together.ai") || baseUrl.includes("api.together.xyz");
|
||||
const isMoonshot = provider === "moonshotai" || provider === "moonshotai-cn" || baseUrl.includes("api.moonshot.");
|
||||
const isCloudflareWorkersAI = provider === "cloudflare-workers-ai" || baseUrl.includes("api.cloudflare.com");
|
||||
const isCloudflareAiGateway = provider === "cloudflare-ai-gateway" || baseUrl.includes("gateway.ai.cloudflare.com");
|
||||
@@ -1059,6 +1070,7 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
|
||||
baseUrl.includes("cerebras.ai") ||
|
||||
provider === "xai" ||
|
||||
baseUrl.includes("api.x.ai") ||
|
||||
isTogether ||
|
||||
baseUrl.includes("chutes.ai") ||
|
||||
baseUrl.includes("deepseek.com") ||
|
||||
isZai ||
|
||||
@@ -1068,7 +1080,7 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
|
||||
isCloudflareWorkersAI ||
|
||||
isCloudflareAiGateway;
|
||||
|
||||
const useMaxTokens = baseUrl.includes("chutes.ai") || isMoonshot || isCloudflareAiGateway;
|
||||
const useMaxTokens = baseUrl.includes("chutes.ai") || isMoonshot || isCloudflareAiGateway || isTogether;
|
||||
|
||||
const isGrok = provider === "xai" || baseUrl.includes("api.x.ai");
|
||||
const isDeepSeek = provider === "deepseek" || baseUrl.includes("deepseek.com");
|
||||
@@ -1077,7 +1089,7 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
|
||||
return {
|
||||
supportsStore: !isNonStandard,
|
||||
supportsDeveloperRole: !isNonStandard,
|
||||
supportsReasoningEffort: !isGrok && !isZai && !isMoonshot && !isCloudflareAiGateway,
|
||||
supportsReasoningEffort: !isGrok && !isZai && !isMoonshot && !isTogether && !isCloudflareAiGateway,
|
||||
supportsUsageInStreaming: true,
|
||||
maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens",
|
||||
requiresToolResultName: false,
|
||||
@@ -1088,16 +1100,18 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
|
||||
? "deepseek"
|
||||
: isZai
|
||||
? "zai"
|
||||
: provider === "openrouter" || baseUrl.includes("openrouter.ai")
|
||||
? "openrouter"
|
||||
: "openai",
|
||||
: isTogether
|
||||
? "together"
|
||||
: provider === "openrouter" || baseUrl.includes("openrouter.ai")
|
||||
? "openrouter"
|
||||
: "openai",
|
||||
openRouterRouting: {},
|
||||
vercelGatewayRouting: {},
|
||||
zaiToolStream: false,
|
||||
supportsStrictMode: !isMoonshot && !isCloudflareAiGateway,
|
||||
supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway,
|
||||
cacheControlFormat,
|
||||
sendSessionAffinityHeaders: false,
|
||||
supportsLongCacheRetention: !(isCloudflareWorkersAI || isCloudflareAiGateway),
|
||||
supportsLongCacheRetention: !(isTogether || isCloudflareWorkersAI || isCloudflareAiGateway),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export type KnownProvider =
|
||||
| "moonshotai-cn"
|
||||
| "huggingface"
|
||||
| "fireworks"
|
||||
| "together"
|
||||
| "opencode"
|
||||
| "opencode-go"
|
||||
| "kimi-coding"
|
||||
@@ -380,8 +381,8 @@ export interface OpenAICompletionsCompat {
|
||||
requiresThinkingAsText?: boolean;
|
||||
/** Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. */
|
||||
requiresReasoningContentOnAssistantMessages?: boolean;
|
||||
/** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "deepseek" uses thinking: { type } plus reasoning_effort, "zai" uses top-level enable_thinking: boolean, "qwen" uses top-level enable_thinking: boolean, and "qwen-chat-template" uses chat_template_kwargs.enable_thinking. Default: "openai". */
|
||||
thinkingFormat?: "openai" | "openrouter" | "deepseek" | "zai" | "qwen" | "qwen-chat-template";
|
||||
/** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "deepseek" uses thinking: { type } plus reasoning_effort, "together" uses reasoning: { enabled } plus reasoning_effort when supported, "zai" uses top-level enable_thinking: boolean, "qwen" uses top-level enable_thinking: boolean, and "qwen-chat-template" uses chat_template_kwargs.enable_thinking. Default: "openai". */
|
||||
thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "zai" | "qwen" | "qwen-chat-template";
|
||||
/** OpenRouter-specific routing preferences. Only used when baseUrl points to OpenRouter. */
|
||||
openRouterRouting?: OpenRouterRouting;
|
||||
/** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { AssistantMessage } from "../types.js";
|
||||
* - xAI: "This model's maximum prompt length is 131072 but the request contains 537812 tokens"
|
||||
* - Groq: "Please reduce the length of the messages or completion"
|
||||
* - OpenRouter: "This endpoint's maximum context length is X tokens. However, you requested about Y tokens"
|
||||
* - Together AI: "The input (X tokens) is longer than the model's context length (Y tokens)."
|
||||
* - llama.cpp: "the request exceeds the available context size, try increasing it"
|
||||
* - LM Studio: "tokens to keep from the initial prompt is greater than the context length"
|
||||
* - GitHub Copilot: "prompt token count of X exceeds the limit of Y"
|
||||
@@ -37,6 +38,7 @@ const OVERFLOW_PATTERNS = [
|
||||
/maximum prompt length is \d+/i, // xAI (Grok)
|
||||
/reduce the length of the messages/i, // Groq
|
||||
/maximum context length is \d+ tokens/i, // OpenRouter (all backends)
|
||||
/input \(\d+ tokens\) is longer than the model'?s context length \(\d+ tokens\)/i, // Together AI
|
||||
/exceeds the limit of \d+/i, // GitHub Copilot
|
||||
/exceeds the available context size/i, // llama.cpp server
|
||||
/greater than the context length/i, // LM Studio
|
||||
@@ -86,6 +88,7 @@ const NON_OVERFLOW_PATTERNS = [
|
||||
* - Cerebras: 400/413 status code (no body)
|
||||
* - Mistral: "Prompt contains X tokens ... too large for model with Y maximum context length"
|
||||
* - OpenRouter (all backends): "maximum context length is X tokens"
|
||||
* - Together AI: "The input (X tokens) is longer than the model's context length (Y tokens)."
|
||||
* - llama.cpp: "exceeds the available context size"
|
||||
* - LM Studio: "greater than the context length"
|
||||
* - Kimi For Coding: "exceeded model token limit: X (requested: Y)"
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added Together AI to built-in provider setup, `/login` API-key auth, and default model resolution ([#3624](https://github.com/earendil-works/pi-mono/pull/3624) by [@Nutlope](https://github.com/Nutlope)).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed keybinding hints to show Option instead of Alt on macOS ([#4289](https://github.com/earendil-works/pi/issues/4289)).
|
||||
|
||||
@@ -127,6 +127,7 @@ For each built-in provider, pi maintains a list of tool-capable models, updated
|
||||
- OpenCode Go
|
||||
- Hugging Face
|
||||
- Fireworks
|
||||
- Together AI
|
||||
- Kimi For Coding
|
||||
- MiniMax
|
||||
- Xiaomi MiMo
|
||||
|
||||
@@ -226,7 +226,7 @@ models: [{
|
||||
}]
|
||||
```
|
||||
|
||||
Use `qwen-chat-template` instead for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`.
|
||||
Use `openrouter` for OpenRouter-style `reasoning: { effort }` controls. Use `together` for Together-style `reasoning: { enabled }` controls; with `supportsReasoningEffort`, it also sends `reasoning_effort`. Use `qwen-chat-template` instead for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`.
|
||||
Use `cacheControlFormat: "anthropic"` for OpenAI-compatible providers that expose Anthropic-style prompt caching via `cache_control` on the system prompt, last tool definition, and last user/assistant text content.
|
||||
|
||||
> Migration note: Mistral moved from `openai-completions` to `mistral-conversations`.
|
||||
@@ -636,11 +636,11 @@ interface ProviderModelConfig {
|
||||
requiresAssistantAfterToolResult?: boolean;
|
||||
requiresThinkingAsText?: boolean;
|
||||
requiresReasoningContentOnAssistantMessages?: boolean;
|
||||
thinkingFormat?: "openai" | "deepseek" | "zai" | "qwen" | "qwen-chat-template";
|
||||
thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "zai" | "qwen" | "qwen-chat-template";
|
||||
cacheControlFormat?: "anthropic";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
`deepseek` sends `thinking: { type: "enabled" | "disabled" }` and `reasoning_effort` when enabled. `qwen` is for DashScope-style top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`.
|
||||
`openrouter` sends `reasoning: { effort }`. `deepseek` sends `thinking: { type: "enabled" | "disabled" }` and `reasoning_effort` when enabled. `together` sends `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` is for DashScope-style top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`.
|
||||
`cacheControlFormat: "anthropic"` applies Anthropic-style `cache_control` markers to the system prompt, last tool definition, and last user/assistant text content.
|
||||
|
||||
@@ -381,14 +381,14 @@ For providers with partial OpenAI compatibility, use the `compat` field.
|
||||
| `requiresAssistantAfterToolResult` | Insert an assistant message before a user message after tool results |
|
||||
| `requiresThinkingAsText` | Convert thinking blocks to plain text |
|
||||
| `requiresReasoningContentOnAssistantMessages` | Include empty `reasoning_content` on all replayed assistant messages when reasoning is enabled |
|
||||
| `thinkingFormat` | Use `reasoning_effort`, `deepseek`, `zai`, `qwen`, or `qwen-chat-template` thinking parameters |
|
||||
| `thinkingFormat` | Use `reasoning_effort`, `openrouter`, `deepseek`, `together`, `zai`, `qwen`, or `qwen-chat-template` thinking parameters |
|
||||
| `cacheControlFormat` | Use Anthropic-style `cache_control` markers on the system prompt, last tool definition, and last user/assistant text content. Currently only `anthropic` is supported. |
|
||||
| `supportsStrictMode` | Include the `strict` field in tool definitions |
|
||||
| `supportsLongCacheRetention` | Whether the provider accepts long cache retention when cache retention is `long`: `prompt_cache_retention: "24h"` for OpenAI prompt caching, or `cache_control.ttl: "1h"` when `cacheControlFormat` is `anthropic`. Default: `true`. |
|
||||
| `openRouterRouting` | OpenRouter provider routing preferences. This object is sent as-is in the `provider` field of the [OpenRouter API request](https://openrouter.ai/docs/guides/routing/provider-selection). |
|
||||
| `vercelGatewayRouting` | Vercel AI Gateway routing config for provider selection (`only`, `order`) |
|
||||
|
||||
`qwen` uses top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that require `chat_template_kwargs.enable_thinking`.
|
||||
`openrouter` uses `reasoning: { effort }`. `together` uses `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` uses top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that require `chat_template_kwargs.enable_thinking`.
|
||||
|
||||
`cacheControlFormat: "anthropic"` is for OpenAI-compatible providers that expose Anthropic-style prompt caching through `cache_control` markers on text content and tool definitions.
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ pi
|
||||
| OpenCode Go | `OPENCODE_API_KEY` | `opencode-go` |
|
||||
| Hugging Face | `HF_TOKEN` | `huggingface` |
|
||||
| Fireworks | `FIREWORKS_API_KEY` | `fireworks` |
|
||||
| Together AI | `TOGETHER_API_KEY` | `together` |
|
||||
| Kimi For Coding | `KIMI_API_KEY` | `kimi-coding` |
|
||||
| MiniMax | `MINIMAX_API_KEY` | `minimax` |
|
||||
| MiniMax (China) | `MINIMAX_CN_API_KEY` | `minimax-cn` |
|
||||
@@ -88,6 +89,7 @@ Store credentials in `~/.pi/agent/auth.json`:
|
||||
"google": { "type": "api_key", "key": "..." },
|
||||
"opencode": { "type": "api_key", "key": "..." },
|
||||
"opencode-go": { "type": "api_key", "key": "..." },
|
||||
"together": { "type": "api_key", "key": "..." },
|
||||
"xiaomi": { "type": "api_key", "key": "..." },
|
||||
"xiaomi-token-plan-cn": { "type": "api_key", "key": "..." },
|
||||
"xiaomi-token-plan-ams": { "type": "api_key", "key": "..." },
|
||||
|
||||
@@ -314,6 +314,7 @@ ${chalk.bold("Environment Variables:")}
|
||||
CEREBRAS_API_KEY - Cerebras API key
|
||||
XAI_API_KEY - xAI Grok API key
|
||||
FIREWORKS_API_KEY - Fireworks API key
|
||||
TOGETHER_API_KEY - Together AI API key
|
||||
OPENROUTER_API_KEY - OpenRouter API key
|
||||
AI_GATEWAY_API_KEY - Vercel AI Gateway API key
|
||||
ZAI_API_KEY - ZAI API key
|
||||
|
||||
@@ -105,6 +105,7 @@ const OpenAICompletionsCompatSchema = Type.Object({
|
||||
Type.Union([
|
||||
Type.Literal("openai"),
|
||||
Type.Literal("openrouter"),
|
||||
Type.Literal("together"),
|
||||
Type.Literal("deepseek"),
|
||||
Type.Literal("zai"),
|
||||
Type.Literal("qwen"),
|
||||
|
||||
@@ -34,6 +34,7 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
|
||||
"moonshotai-cn": "kimi-k2.6",
|
||||
huggingface: "moonshotai/Kimi-K2.6",
|
||||
fireworks: "accounts/fireworks/models/kimi-k2p6",
|
||||
together: "moonshotai/Kimi-K2.6",
|
||||
opencode: "kimi-k2.6",
|
||||
"opencode-go": "kimi-k2.6",
|
||||
"kimi-coding": "kimi-for-coding",
|
||||
|
||||
@@ -21,6 +21,7 @@ export const BUILT_IN_PROVIDER_DISPLAY_NAMES: Record<string, string> = {
|
||||
"opencode-go": "OpenCode Go",
|
||||
openai: "OpenAI",
|
||||
openrouter: "OpenRouter",
|
||||
together: "Together AI",
|
||||
"vercel-ai-gateway": "Vercel AI Gateway",
|
||||
xai: "xAI",
|
||||
zai: "ZAI",
|
||||
|
||||
Reference in New Issue
Block a user