feat(ai): add Together AI provider

This commit is contained in:
Mario Zechner
2026-05-08 16:44:18 +02:00
parent 9751057be9
commit 7adb8e7634
30 changed files with 714 additions and 56 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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` |

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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),
};
}

View File

@@ -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. */

View File

@@ -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)"

View File

@@ -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");

View File

@@ -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,

View File

@@ -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

View File

@@ -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");

View File

@@ -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");

View File

@@ -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);

View File

@@ -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");

View 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");
});
});

View File

@@ -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");

View File

@@ -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");

View File

@@ -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
// =========================================================================

View File

@@ -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");

View File

@@ -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)).

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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": "..." },

View File

@@ -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

View File

@@ -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"),

View File

@@ -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",

View File

@@ -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",