mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
Merge branch 'main' into approvals
This commit is contained in:
+87
@@ -0,0 +1,87 @@
|
||||
# Security Policy
|
||||
|
||||
This document should guide you about understanding the security concept behind
|
||||
Pi and also where the boundaries are.
|
||||
|
||||
In general Pi is a coding agent that runs locally within the security boundary
|
||||
of the user that is running it. It's the responsibiltiy of the user to monitor
|
||||
its operations or to contain it within a container, virtual machine or other
|
||||
Sandbox solution.
|
||||
|
||||
Pi treats the local user account and files writable by that account as inside
|
||||
the same trust boundary as the Pi process itself. If an attacker can modify files
|
||||
under the user's home directory, workspace, shell startup files, environment, or
|
||||
Pi configuration, they can generally influence Pi or other local developer tools.
|
||||
Reports that depend on such prior local write access are not security
|
||||
vulnerabilities unless they demonstrate how Pi grants that write access or crosses
|
||||
an operating-system privilege boundary.
|
||||
|
||||
Pi relies on users installing trustworthy extensions and loading trustworthy
|
||||
skills and only to use pi within trusted repositories. This is because files
|
||||
like `AGENTS.md` or instructions in comments can be used to prompt inject the
|
||||
coding agent trivially and this cannot be protected against.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you believe you found a security vulnerability in pi or another package in
|
||||
this repository, please report it privately by either:
|
||||
|
||||
- Emailing `security@earendil.com`, or
|
||||
- Opening a private report through GitHub Security Advisories for this repository
|
||||
|
||||
Please include:
|
||||
|
||||
- A description of the issue and its impact
|
||||
- Steps to reproduce, proof of concept, or relevant logs
|
||||
- Affected package, version, commit, or configuration
|
||||
- Any known mitigations
|
||||
|
||||
Do not open a public issue for security-sensitive reports. We will review
|
||||
reports and coordinate disclosure as appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
Security issues in the distributed packages, command-line tools, APIs, and
|
||||
repository code are in scope as well as earendil operated infrastricture
|
||||
on `pi.dev`.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- Local code execution or sandboxing behavior (the Pi coding agent intentionally does not have a sandbox)
|
||||
- Behavior of pi extensions or skills installed by the user
|
||||
- Risks from working in untrusted repositories
|
||||
- Risks from installing untrusted extensions, skills, packages, or tools
|
||||
- Isuses caused by non trustworthy MITM proxies
|
||||
- Public internet exposure of a Pi installation
|
||||
- Prompt injection attacks
|
||||
- Exposed secrets that are third-party/user-controlled credentials
|
||||
- Reports requiring the ability to create, modify, delete, or replace files,
|
||||
directories, symlinks, environment variables, shell configuration, or other
|
||||
user-controlled local state on the target machine. This includes `~/.pi`,
|
||||
`~/.pi/agent/models.json`, workspace files, `AGENTS.md`, skills, extensions,
|
||||
extension configuration, dotfiles, and files synchronized through NFS, roaming
|
||||
profiles, or dotfile managers, unless the report shows how Pi itself grants
|
||||
that access.
|
||||
- Issues caused by intentionally weakened user configuration.
|
||||
- Resource/DOS claims that require trusted local input/config against the pi coding agent.
|
||||
- Reports about malicious model output.
|
||||
- User-approved or user-initiated local actions presented as vulnerabilities.
|
||||
|
||||
## Notes for Reporters
|
||||
|
||||
The most useful reports show a current, reproducible security boundary bypass
|
||||
with demonstrated impact. Reports that only show expected local-agent behavior,
|
||||
prompt injection, or a malicious trusted extension/skill are not security
|
||||
vulnerabilities under this model.
|
||||
|
||||
For example, a report showing that malicious contents written to a trusted Pi
|
||||
configuration file cause Pi to execute commands, load attacker-controlled tools,
|
||||
send credentials to an attacker-controlled endpoint, or otherwise change behavior
|
||||
is out of scope.
|
||||
|
||||
When possible, include the exact affected path, package version or commit SHA,
|
||||
configuration, and a proof of concept against the latest release or latest
|
||||
`main`. For dependency reports, include evidence that the shipped dependency is
|
||||
affected and that the issue is reachable through Pi. For exposed-secret reports,
|
||||
include evidence that the credential is owned by Earendil or grants access to
|
||||
Earendil-operated infrastructure or services.
|
||||
@@ -2,10 +2,17 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added Ant Ling as a built-in OpenAI-compatible provider with Ling 2.6 and Ring 2.6 models.
|
||||
- Added MiniMax-M3 model to the `minimax` and `minimax-cn` direct providers, and removed the hardcoded context-window override that was masking models.dev values ([#5313](https://github.com/earendil-works/pi/issues/5313)).
|
||||
- Added NVIDIA NIM as a built-in OpenAI-compatible provider, exposing public NIM models that support tool use.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Amazon Bedrock requests to replace blank required user/tool-result text with a placeholder and skip blank replay text blocks ([#4975](https://github.com/earendil-works/pi/issues/4975)).
|
||||
- Fixed OpenAI GPT-5.5 generated metadata to omit unsupported minimal thinking ([#5243](https://github.com/earendil-works/pi/issues/5243)).
|
||||
- Fixed OpenRouter Kimi K2.6 thinking replay and preserved developer-role instructions for OpenRouter OpenAI and Anthropic models ([#5309](https://github.com/earendil-works/pi/issues/5309)).
|
||||
- Fixed GitHub Copilot and OpenRouter test model references that became stale after model regeneration.
|
||||
|
||||
## [0.78.0] - 2026-05-29
|
||||
|
||||
@@ -51,9 +51,11 @@ Unified LLM API with automatic model discovery, provider configuration, token an
|
||||
## Supported Providers
|
||||
|
||||
- **OpenAI**
|
||||
- **Ant Ling**
|
||||
- **Azure OpenAI (Responses)**
|
||||
- **OpenAI Codex** (ChatGPT Plus/Pro subscription, requires OAuth, see below)
|
||||
- **DeepSeek**
|
||||
- **NVIDIA NIM**
|
||||
- **Anthropic**
|
||||
- **Google**
|
||||
- **Vertex AI** (Gemini via Vertex AI)
|
||||
@@ -801,7 +803,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, Together AI, etc.** models use the `openai-completions` API (OpenAI-compatible)
|
||||
- **xAI, Cerebras, Groq, NVIDIA NIM, Together AI, etc.** models use the `openai-completions` API (OpenAI-compatible)
|
||||
|
||||
### Querying Providers and Models
|
||||
|
||||
@@ -923,7 +925,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, 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.
|
||||
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, NVIDIA NIM, 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 {
|
||||
@@ -938,7 +940,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' | '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 when supported, '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)
|
||||
thinkingFormat?: 'openai' | 'openrouter' | 'deepseek' | 'together' | 'zai' | 'qwen' | 'qwen-chat-template' | 'string-thinking' | 'ant-ling'; // Format for reasoning param: 'openai' uses reasoning_effort, 'openrouter' uses reasoning: { effort }, 'deepseek' uses thinking: { type } plus reasoning_effort when supported, '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, 'string-thinking' uses top-level thinking, 'ant-ling' uses reasoning: { effort } only for mapped efforts (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: {})
|
||||
@@ -1099,9 +1101,11 @@ In Node.js environments, you can set environment variables to avoid passing API
|
||||
| Provider | Environment Variable(s) |
|
||||
|----------|------------------------|
|
||||
| OpenAI | `OPENAI_API_KEY` |
|
||||
| Ant Ling | `ANT_LING_API_KEY` |
|
||||
| Azure OpenAI | `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_BASE_URL` (e.g. `https://{resource}.openai.azure.com`) or `AZURE_OPENAI_RESOURCE_NAME`. Supports `*.openai.azure.com` and `*.cognitiveservices.azure.com`; root endpoints auto-normalize to `/openai/v1`. Optional: `AZURE_OPENAI_API_VERSION` (default `v1`), `AZURE_OPENAI_DEPLOYMENT_NAME_MAP`. |
|
||||
| Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` |
|
||||
| DeepSeek | `DEEPSEEK_API_KEY` |
|
||||
| NVIDIA NIM | `NVIDIA_API_KEY` |
|
||||
| Google | `GEMINI_API_KEY` |
|
||||
| Vertex AI | `GOOGLE_CLOUD_API_KEY` or `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) + `GOOGLE_CLOUD_LOCATION` + ADC |
|
||||
| Mistral | `MISTRAL_API_KEY` |
|
||||
|
||||
@@ -32,12 +32,17 @@ interface ModelsDevModel {
|
||||
};
|
||||
modalities?: {
|
||||
input?: string[];
|
||||
output?: string[];
|
||||
};
|
||||
provider?: {
|
||||
npm?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface NvidiaNimModelListItem {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface AiGatewayModel {
|
||||
id: string;
|
||||
name?: string;
|
||||
@@ -117,6 +122,38 @@ const TOGETHER_TOGGLE_REASONING_LEVEL_MAP = {
|
||||
|
||||
const AI_GATEWAY_MODELS_URL = "https://ai-gateway.vercel.sh/v1";
|
||||
const AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh";
|
||||
const NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1";
|
||||
const NVIDIA_HEADERS = {
|
||||
"NVCF-POLL-SECONDS": "3600",
|
||||
} as const;
|
||||
const NVIDIA_OPENAI_COMPAT: OpenAICompletionsCompat = {
|
||||
supportsStore: false,
|
||||
supportsDeveloperRole: false,
|
||||
supportsReasoningEffort: false,
|
||||
maxTokensField: "max_tokens",
|
||||
supportsStrictMode: false,
|
||||
supportsLongCacheRetention: false,
|
||||
};
|
||||
const NVIDIA_NIM_UNSUPPORTED_MODELS = new Set([
|
||||
"abacusai/dracarys-llama-3.1-70b-instruct",
|
||||
"bytedance/seed-oss-36b-instruct",
|
||||
"deepseek-ai/deepseek-v4-flash",
|
||||
"deepseek-ai/deepseek-v4-pro",
|
||||
"google/gemma-2-2b-it",
|
||||
"google/gemma-3n-e2b-it",
|
||||
"google/gemma-3n-e4b-it",
|
||||
"google/gemma-4-31b-it",
|
||||
"meta/llama-3.2-1b-instruct",
|
||||
"meta/llama-4-maverick-17b-128e-instruct",
|
||||
"microsoft/phi-4-mini-instruct",
|
||||
"minimaxai/minimax-m2.7",
|
||||
"mistralai/mistral-nemotron",
|
||||
"nvidia/nemotron-mini-4b-instruct",
|
||||
"qwen/qwen3-next-80b-a3b-instruct",
|
||||
"qwen/qwen3.5-397b-a17b",
|
||||
"sarvamai/sarvam-m",
|
||||
"upstage/solar-10.7b-instruct",
|
||||
]);
|
||||
const ZAI_TOOL_STREAM_UNSUPPORTED_MODELS = new Set(["glm-4.5", "glm-4.5-air", "glm-4.5-flash", "glm-4.5v"]);
|
||||
const EAGER_TOOL_INPUT_STREAMING_UNSUPPORTED_ANTHROPIC_MODELS = new Set([
|
||||
"github-copilot:claude-haiku-4.5",
|
||||
@@ -132,6 +169,15 @@ const DEEPSEEK_V4_THINKING_LEVEL_MAP = {
|
||||
xhigh: "max",
|
||||
} as const;
|
||||
|
||||
const ANT_LING_RING_THINKING_LEVEL_MAP = {
|
||||
off: null,
|
||||
minimal: null,
|
||||
low: null,
|
||||
medium: null,
|
||||
high: "high",
|
||||
xhigh: "xhigh",
|
||||
} as const;
|
||||
|
||||
const OPENAI_RESPONSES_NONE_REASONING_MODELS = new Set([
|
||||
"gpt-5.1",
|
||||
"gpt-5.2",
|
||||
@@ -293,6 +339,10 @@ function applyThinkingLevelMetadata(model: Model<any>): void {
|
||||
// OpenCode Zen Grok Build reasons by default but rejects explicit reasoningEffort.
|
||||
mergeThinkingLevelMap(model, { off: null, minimal: null, low: null, medium: null });
|
||||
}
|
||||
if (model.provider === "ant-ling" && model.reasoning) {
|
||||
// Ring reasons by default. Only high/xhigh have documented explicit effort controls.
|
||||
mergeThinkingLevelMap(model, ANT_LING_RING_THINKING_LEVEL_MAP);
|
||||
}
|
||||
}
|
||||
|
||||
function getAnthropicMessagesCompat(provider: string, modelId: string): AnthropicMessagesCompat | undefined {
|
||||
@@ -312,6 +362,30 @@ function getBedrockBaseUrl(modelId: string): string {
|
||||
: "https://bedrock-runtime.us-east-1.amazonaws.com";
|
||||
}
|
||||
|
||||
function normalizeNvidiaModelId(modelId: string): string {
|
||||
return modelId.toLowerCase().replaceAll("_", ".");
|
||||
}
|
||||
|
||||
async function fetchNvidiaNimModelIds(): Promise<Map<string, string>> {
|
||||
try {
|
||||
console.log("Fetching models from NVIDIA NIM API...");
|
||||
const response = await fetch(`${NVIDIA_BASE_URL}/models`);
|
||||
const data = (await response.json()) as { data?: NvidiaNimModelListItem[] };
|
||||
const modelIds = new Map<string, string>();
|
||||
|
||||
for (const model of data.data ?? []) {
|
||||
modelIds.set(model.id, model.id);
|
||||
modelIds.set(normalizeNvidiaModelId(model.id), model.id);
|
||||
}
|
||||
|
||||
console.log(`Fetched ${data.data?.length ?? 0} model IDs from NVIDIA NIM`);
|
||||
return modelIds;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch NVIDIA NIM models:", error);
|
||||
return new Map();
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchOpenRouterModels(): Promise<Model<any>[]> {
|
||||
try {
|
||||
console.log("Fetching models from OpenRouter API...");
|
||||
@@ -435,6 +509,7 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
||||
const data = await response.json();
|
||||
|
||||
const models: Model<any>[] = [];
|
||||
const nvidiaNimModelIds = data.nvidia?.models ? await fetchNvidiaNimModelIds() : new Map<string, string>();
|
||||
|
||||
// Process Amazon Bedrock models
|
||||
if (data["amazon-bedrock"]?.models) {
|
||||
@@ -836,6 +911,40 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
||||
}
|
||||
}
|
||||
|
||||
// Process NVIDIA NIM models
|
||||
if (data.nvidia?.models) {
|
||||
for (const [modelId, model] of Object.entries(data.nvidia.models)) {
|
||||
const m = model as ModelsDevModel;
|
||||
if (m.tool_call !== true) continue;
|
||||
if (!m.modalities?.input?.includes("text")) continue;
|
||||
if (!m.modalities?.output?.includes("text")) continue;
|
||||
|
||||
const liveModelId = nvidiaNimModelIds.get(modelId) ?? nvidiaNimModelIds.get(normalizeNvidiaModelId(modelId));
|
||||
if (!liveModelId) continue;
|
||||
if (NVIDIA_NIM_UNSUPPORTED_MODELS.has(liveModelId)) continue;
|
||||
|
||||
models.push({
|
||||
id: liveModelId,
|
||||
name: m.name || liveModelId,
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: NVIDIA_BASE_URL,
|
||||
headers: { ...NVIDIA_HEADERS },
|
||||
reasoning: m.reasoning === true,
|
||||
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: NVIDIA_OPENAI_COMPAT,
|
||||
contextWindow: m.limit?.context || 4096,
|
||||
maxTokens: m.limit?.output || 4096,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process Together AI models
|
||||
const togetherProvider = data.together ?? data.togetherai ?? data["together-ai"];
|
||||
if (togetherProvider?.models) {
|
||||
@@ -1247,7 +1356,11 @@ async function generateModels() {
|
||||
candidate.maxTokens = 4096;
|
||||
}
|
||||
if (candidate.provider === "openrouter" && candidate.id.startsWith("moonshotai/kimi-k2.6")) {
|
||||
candidate.compat = { ...candidate.compat, supportsDeveloperRole: false };
|
||||
candidate.compat = {
|
||||
...candidate.compat,
|
||||
supportsDeveloperRole: false,
|
||||
requiresReasoningContentOnAssistantMessages: true,
|
||||
};
|
||||
}
|
||||
if (candidate.provider === "openrouter" && candidate.id === "z-ai/glm-5") {
|
||||
candidate.cost.input = 0.6;
|
||||
@@ -1543,6 +1656,56 @@ async function generateModels() {
|
||||
];
|
||||
allModels.push(...deepseekV4Models);
|
||||
|
||||
const antLingCompat: OpenAICompletionsCompat = {
|
||||
supportsStore: false,
|
||||
supportsDeveloperRole: false,
|
||||
supportsReasoningEffort: false,
|
||||
maxTokensField: "max_tokens",
|
||||
supportsLongCacheRetention: false,
|
||||
};
|
||||
const antLingModels: Model<"openai-completions">[] = [
|
||||
{
|
||||
id: "Ling-2.6-flash",
|
||||
name: "Ling 2.6 Flash",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://api.ant-ling.com/v1",
|
||||
provider: "ant-ling",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0.01, output: 0.02, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 262144,
|
||||
maxTokens: 65536,
|
||||
compat: antLingCompat,
|
||||
},
|
||||
{
|
||||
id: "Ling-2.6-1T",
|
||||
name: "Ling 2.6 1T",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://api.ant-ling.com/v1",
|
||||
provider: "ant-ling",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0.06, output: 0.25, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 262144,
|
||||
maxTokens: 65536,
|
||||
compat: antLingCompat,
|
||||
},
|
||||
{
|
||||
id: "Ring-2.6-1T",
|
||||
name: "Ring 2.6 1T",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://api.ant-ling.com/v1",
|
||||
provider: "ant-ling",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0.06, output: 0.25, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 262144,
|
||||
maxTokens: 65536,
|
||||
compat: { ...antLingCompat, thinkingFormat: "ant-ling" },
|
||||
},
|
||||
];
|
||||
allModels.push(...antLingModels);
|
||||
|
||||
for (const candidate of allModels) {
|
||||
if (candidate.api === "openai-completions" && candidate.id.includes("deepseek-v4")) {
|
||||
candidate.compat = {
|
||||
@@ -1557,17 +1720,7 @@ async function generateModels() {
|
||||
}
|
||||
}
|
||||
|
||||
const minimaxDirectSupportedIds = new Set(["MiniMax-M2.7", "MiniMax-M2.7-highspeed"]);
|
||||
|
||||
for (const candidate of allModels) {
|
||||
if (
|
||||
(candidate.provider === "minimax" || candidate.provider === "minimax-cn") &&
|
||||
minimaxDirectSupportedIds.has(candidate.id)
|
||||
) {
|
||||
candidate.contextWindow = 204800;
|
||||
candidate.maxTokens = 131072;
|
||||
}
|
||||
}
|
||||
const minimaxDirectSupportedIds = new Set(["MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M3"]);
|
||||
|
||||
for (let i = allModels.length - 1; i >= 0; i--) {
|
||||
const candidate = allModels[i];
|
||||
|
||||
@@ -99,8 +99,10 @@ function getApiKeyEnvVars(provider: string): readonly string[] | undefined {
|
||||
}
|
||||
|
||||
const envMap: Record<string, string> = {
|
||||
"ant-ling": "ANT_LING_API_KEY",
|
||||
openai: "OPENAI_API_KEY",
|
||||
"azure-openai-responses": "AZURE_OPENAI_API_KEY",
|
||||
nvidia: "NVIDIA_API_KEY",
|
||||
deepseek: "DEEPSEEK_API_KEY",
|
||||
google: "GEMINI_API_KEY",
|
||||
"google-vertex": "GOOGLE_CLOUD_API_KEY",
|
||||
|
||||
@@ -1552,6 +1552,63 @@ export const MODELS = {
|
||||
maxTokens: 101376,
|
||||
} satisfies Model<"bedrock-converse-stream">,
|
||||
},
|
||||
"ant-ling": {
|
||||
"Ling-2.6-1T": {
|
||||
id: "Ling-2.6-1T",
|
||||
name: "Ling 2.6 1T",
|
||||
api: "openai-completions",
|
||||
provider: "ant-ling",
|
||||
baseUrl: "https://api.ant-ling.com/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false},
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.06,
|
||||
output: 0.25,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 65536,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"Ling-2.6-flash": {
|
||||
id: "Ling-2.6-flash",
|
||||
name: "Ling 2.6 Flash",
|
||||
api: "openai-completions",
|
||||
provider: "ant-ling",
|
||||
baseUrl: "https://api.ant-ling.com/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false},
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.01,
|
||||
output: 0.02,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 65536,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"Ring-2.6-1T": {
|
||||
id: "Ring-2.6-1T",
|
||||
name: "Ring 2.6 1T",
|
||||
api: "openai-completions",
|
||||
provider: "ant-ling",
|
||||
baseUrl: "https://api.ant-ling.com/v1",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"thinkingFormat":"ant-ling"},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"},
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.06,
|
||||
output: 0.25,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 65536,
|
||||
} satisfies Model<"openai-completions">,
|
||||
},
|
||||
"anthropic": {
|
||||
"claude-3-5-haiku-20241022": {
|
||||
id: "claude-3-5-haiku-20241022",
|
||||
@@ -5564,6 +5621,23 @@ export const MODELS = {
|
||||
contextWindow: 204800,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"MiniMax-M3": {
|
||||
id: "MiniMax-M3",
|
||||
name: "MiniMax-M3",
|
||||
api: "anthropic-messages",
|
||||
provider: "minimax",
|
||||
baseUrl: "https://api.minimax.io/anthropic",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.3,
|
||||
output: 1.2,
|
||||
cacheRead: 0.06,
|
||||
cacheWrite: 0.375,
|
||||
},
|
||||
contextWindow: 512000,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
},
|
||||
"minimax-cn": {
|
||||
"MiniMax-M2.7": {
|
||||
@@ -5600,6 +5674,23 @@ export const MODELS = {
|
||||
contextWindow: 204800,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"MiniMax-M3": {
|
||||
id: "MiniMax-M3",
|
||||
name: "MiniMax-M3",
|
||||
api: "anthropic-messages",
|
||||
provider: "minimax-cn",
|
||||
baseUrl: "https://api.minimaxi.com/anthropic",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.3,
|
||||
output: 1.2,
|
||||
cacheRead: 0.06,
|
||||
cacheWrite: 0.375,
|
||||
},
|
||||
contextWindow: 512000,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
},
|
||||
"mistral": {
|
||||
"codestral-latest": {
|
||||
@@ -6335,6 +6426,388 @@ export const MODELS = {
|
||||
maxTokens: 262144,
|
||||
} satisfies Model<"openai-completions">,
|
||||
},
|
||||
"nvidia": {
|
||||
"meta/llama-3.1-70b-instruct": {
|
||||
id: "meta/llama-3.1-70b-instruct",
|
||||
name: "Llama 3.1 70b Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meta/llama-3.1-8b-instruct": {
|
||||
id: "meta/llama-3.1-8b-instruct",
|
||||
name: "Llama 3.1 8B Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 16000,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meta/llama-3.2-11b-vision-instruct": {
|
||||
id: "meta/llama-3.2-11b-vision-instruct",
|
||||
name: "Llama 3.2 11b Vision Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meta/llama-3.2-90b-vision-instruct": {
|
||||
id: "meta/llama-3.2-90b-vision-instruct",
|
||||
name: "Llama-3.2-90B-Vision-Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
maxTokens: 8192,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meta/llama-3.3-70b-instruct": {
|
||||
id: "meta/llama-3.3-70b-instruct",
|
||||
name: "Llama 3.3 70b Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"mistralai/mistral-large-3-675b-instruct-2512": {
|
||||
id: "mistralai/mistral-large-3-675b-instruct-2512",
|
||||
name: "Mistral Large 3 675B Instruct 2512",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 262144,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"mistralai/mistral-small-4-119b-2603": {
|
||||
id: "mistralai/mistral-small-4-119b-2603",
|
||||
name: "mistral-small-4-119b-2603",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
maxTokens: 8192,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"moonshotai/kimi-k2.6": {
|
||||
id: "moonshotai/kimi-k2.6",
|
||||
name: "Kimi K2.6",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 262144,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"nvidia/llama-3.3-nemotron-super-49b-v1": {
|
||||
id: "nvidia/llama-3.3-nemotron-super-49b-v1",
|
||||
name: "Llama 3.3 Nemotron Super 49B v1",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"nvidia/llama-3.3-nemotron-super-49b-v1.5": {
|
||||
id: "nvidia/llama-3.3-nemotron-super-49b-v1.5",
|
||||
name: "Llama 3.3 Nemotron Super 49B v1.5",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"nvidia/nemotron-3-nano-30b-a3b": {
|
||||
id: "nvidia/nemotron-3-nano-30b-a3b",
|
||||
name: "nemotron-3-nano-30b-a3b",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning": {
|
||||
id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
|
||||
name: "Nemotron 3 Nano Omni",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 256000,
|
||||
maxTokens: 65536,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"nvidia/nemotron-3-super-120b-a12b": {
|
||||
id: "nvidia/nemotron-3-super-120b-a12b",
|
||||
name: "Nemotron 3 Super",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.2,
|
||||
output: 0.8,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 262144,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"nvidia/nvidia-nemotron-nano-9b-v2": {
|
||||
id: "nvidia/nvidia-nemotron-nano-9b-v2",
|
||||
name: "nvidia-nemotron-nano-9b-v2",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"openai/gpt-oss-20b": {
|
||||
id: "openai/gpt-oss-20b",
|
||||
name: "GPT OSS 20B",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 32768,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen/qwen3-coder-480b-a35b-instruct": {
|
||||
id: "qwen/qwen3-coder-480b-a35b-instruct",
|
||||
name: "Qwen3 Coder 480B A35B Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 66536,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen/qwen3.5-122b-a10b": {
|
||||
id: "qwen/qwen3.5-122b-a10b",
|
||||
name: "Qwen3.5 122B-A10B",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 65536,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"stepfun-ai/step-3.5-flash": {
|
||||
id: "stepfun-ai/step-3.5-flash",
|
||||
name: "Step 3.5 Flash",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 256000,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"stepfun-ai/step-3.7-flash": {
|
||||
id: "stepfun-ai/step-3.7-flash",
|
||||
name: "Step 3.7 Flash",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 256000,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"z-ai/glm-5.1": {
|
||||
id: "z-ai/glm-5.1",
|
||||
name: "GLM-5.1",
|
||||
api: "openai-completions",
|
||||
provider: "nvidia",
|
||||
baseUrl: "https://integrate.api.nvidia.com/v1",
|
||||
headers: {"NVCF-POLL-SECONDS":"3600"},
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false},
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
},
|
||||
"openai": {
|
||||
"gpt-4": {
|
||||
id: "gpt-4",
|
||||
@@ -9307,9 +9780,9 @@ export const MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.3,
|
||||
output: 2.5,
|
||||
cacheRead: 0.06,
|
||||
input: 0.075,
|
||||
output: 0.625,
|
||||
cacheRead: 0.015,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
@@ -9899,7 +10372,7 @@ export const MODELS = {
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
compat: {"supportsDeveloperRole":false},
|
||||
compat: {"supportsDeveloperRole":false,"requiresReasoningContentOnAssistantMessages":true},
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
@@ -9917,7 +10390,7 @@ export const MODELS = {
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
compat: {"supportsDeveloperRole":false},
|
||||
compat: {"supportsDeveloperRole":false,"requiresReasoningContentOnAssistantMessages":true},
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
|
||||
@@ -578,6 +578,11 @@ function buildParams(
|
||||
} else if (model.thinkingLevelMap?.off !== null) {
|
||||
openRouterParams.reasoning = { effort: model.thinkingLevelMap?.off ?? "none" };
|
||||
}
|
||||
} else if (compat.thinkingFormat === "ant-ling" && model.reasoning && options?.reasoningEffort) {
|
||||
const effort = model.thinkingLevelMap?.[options.reasoningEffort];
|
||||
if (typeof effort === "string") {
|
||||
(params as typeof params & { reasoning?: { effort: string } }).reasoning = { effort };
|
||||
}
|
||||
} else if (compat.thinkingFormat === "together" && model.reasoning) {
|
||||
const togetherParams = params as Omit<typeof params, "reasoning_effort"> & {
|
||||
reasoning?: { enabled: boolean };
|
||||
@@ -1078,8 +1083,11 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
|
||||
const isOpenRouter = provider === "openrouter" || baseUrl.includes("openrouter.ai");
|
||||
const isCloudflareWorkersAI = provider === "cloudflare-workers-ai" || baseUrl.includes("api.cloudflare.com");
|
||||
const isCloudflareAiGateway = provider === "cloudflare-ai-gateway" || baseUrl.includes("gateway.ai.cloudflare.com");
|
||||
const isNvidia = provider === "nvidia" || baseUrl.includes("integrate.api.nvidia.com");
|
||||
const isAntLing = provider === "ant-ling" || baseUrl.includes("api.ant-ling.com");
|
||||
|
||||
const isNonStandard =
|
||||
isNvidia ||
|
||||
provider === "cerebras" ||
|
||||
baseUrl.includes("cerebras.ai") ||
|
||||
provider === "xai" ||
|
||||
@@ -1092,18 +1100,23 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
|
||||
provider === "opencode" ||
|
||||
baseUrl.includes("opencode.ai") ||
|
||||
isCloudflareWorkersAI ||
|
||||
isCloudflareAiGateway;
|
||||
isCloudflareAiGateway ||
|
||||
isAntLing;
|
||||
|
||||
const useMaxTokens = baseUrl.includes("chutes.ai") || isMoonshot || isCloudflareAiGateway || isTogether;
|
||||
const useMaxTokens =
|
||||
baseUrl.includes("chutes.ai") || isMoonshot || isCloudflareAiGateway || isTogether || isNvidia || isAntLing;
|
||||
|
||||
const isGrok = provider === "xai" || baseUrl.includes("api.x.ai");
|
||||
const isDeepSeek = provider === "deepseek" || baseUrl.includes("deepseek.com");
|
||||
const isOpenRouterDeveloperRoleModel =
|
||||
isOpenRouter && (model.id.startsWith("anthropic/") || model.id.startsWith("openai/"));
|
||||
const cacheControlFormat = provider === "openrouter" && model.id.startsWith("anthropic/") ? "anthropic" : undefined;
|
||||
|
||||
return {
|
||||
supportsStore: !isNonStandard,
|
||||
supportsDeveloperRole: !isNonStandard && !isOpenRouter,
|
||||
supportsReasoningEffort: !isGrok && !isZai && !isMoonshot && !isTogether && !isCloudflareAiGateway,
|
||||
supportsDeveloperRole: isOpenRouterDeveloperRoleModel || (!isNonStandard && !isOpenRouter),
|
||||
supportsReasoningEffort:
|
||||
!isGrok && !isZai && !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia && !isAntLing,
|
||||
supportsUsageInStreaming: true,
|
||||
maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens",
|
||||
requiresToolResultName: false,
|
||||
@@ -1116,16 +1129,24 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
|
||||
? "zai"
|
||||
: isTogether
|
||||
? "together"
|
||||
: isOpenRouter
|
||||
? "openrouter"
|
||||
: "openai",
|
||||
: isAntLing
|
||||
? "ant-ling"
|
||||
: isOpenRouter
|
||||
? "openrouter"
|
||||
: "openai",
|
||||
openRouterRouting: {},
|
||||
vercelGatewayRouting: {},
|
||||
zaiToolStream: false,
|
||||
supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway,
|
||||
supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia,
|
||||
cacheControlFormat,
|
||||
sendSessionAffinityHeaders: false,
|
||||
supportsLongCacheRetention: !(isTogether || isCloudflareWorkersAI || isCloudflareAiGateway),
|
||||
supportsLongCacheRetention: !(
|
||||
isTogether ||
|
||||
isCloudflareWorkersAI ||
|
||||
isCloudflareAiGateway ||
|
||||
isNvidia ||
|
||||
isAntLing
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -22,12 +22,14 @@ export type ImagesApi = KnownImagesApi | (string & {});
|
||||
|
||||
export type KnownProvider =
|
||||
| "amazon-bedrock"
|
||||
| "ant-ling"
|
||||
| "anthropic"
|
||||
| "google"
|
||||
| "google-vertex"
|
||||
| "openai"
|
||||
| "azure-openai-responses"
|
||||
| "openai-codex"
|
||||
| "nvidia"
|
||||
| "deepseek"
|
||||
| "github-copilot"
|
||||
| "xai"
|
||||
@@ -389,7 +391,7 @@ 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 when supported, "together" uses reasoning: { enabled } plus reasoning_effort when supported, "zai" uses top-level enable_thinking: boolean, "qwen" uses top-level enable_thinking: boolean, "qwen-chat-template" uses chat_template_kwargs.enable_thinking, and "string-thinking" uses top-level thinking: string. Default: "openai". */
|
||||
/** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "deepseek" uses thinking: { type } plus reasoning_effort when supported, "together" uses reasoning: { enabled } plus reasoning_effort when supported, "zai" uses top-level enable_thinking: boolean, "qwen" uses top-level enable_thinking: boolean, "qwen-chat-template" uses chat_template_kwargs.enable_thinking, "string-thinking" uses top-level thinking: string, and "ant-ling" uses reasoning: { effort } only when the mapped effort is non-null. Default: "openai". */
|
||||
thinkingFormat?:
|
||||
| "openai"
|
||||
| "openrouter"
|
||||
@@ -398,7 +400,8 @@ export interface OpenAICompletionsCompat {
|
||||
| "zai"
|
||||
| "qwen"
|
||||
| "qwen-chat-template"
|
||||
| "string-thinking";
|
||||
| "string-thinking"
|
||||
| "ant-ling";
|
||||
/** 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. */
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Type } from "typebox";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { getModel } from "../src/models.ts";
|
||||
import { convertMessages } from "../src/providers/openai-completions.ts";
|
||||
import { streamSimple } from "../src/stream.ts";
|
||||
import { stream, streamSimple } from "../src/stream.ts";
|
||||
import type { AssistantMessage, Model, Tool, ToolResultMessage } from "../src/types.ts";
|
||||
|
||||
const mockState = vi.hoisted(() => ({
|
||||
@@ -817,7 +817,7 @@ describe("openai-completions tool_choice", () => {
|
||||
expect(writeCall).not.toHaveProperty("partialArgs");
|
||||
});
|
||||
|
||||
it("uses system messages for OpenRouter reasoning model instructions", async () => {
|
||||
it("uses system messages for non-OpenAI/Anthropic OpenRouter reasoning model instructions", async () => {
|
||||
const model = getModel("openrouter", "deepseek/deepseek-v4-pro")!;
|
||||
let payload: unknown;
|
||||
|
||||
@@ -839,6 +839,33 @@ describe("openai-completions tool_choice", () => {
|
||||
expect(params.messages?.[0]?.role).toBe("system");
|
||||
});
|
||||
|
||||
it("keeps developer messages for OpenAI and Anthropic OpenRouter reasoning model instructions", async () => {
|
||||
for (const model of [
|
||||
getModel("openrouter", "openai/gpt-5.2-codex"),
|
||||
getModel("openrouter", "anthropic/claude-sonnet-4.5"),
|
||||
]) {
|
||||
expect(model).toBeDefined();
|
||||
let payload: unknown;
|
||||
|
||||
await streamSimple(
|
||||
model!,
|
||||
{
|
||||
systemPrompt: "Follow instructions.",
|
||||
messages: [{ role: "user", content: "Hi", timestamp: Date.now() }],
|
||||
},
|
||||
{
|
||||
apiKey: "test",
|
||||
onPayload: (params: unknown) => {
|
||||
payload = params;
|
||||
},
|
||||
},
|
||||
).result();
|
||||
|
||||
const params = payload as { messages?: Array<{ role?: string }> };
|
||||
expect(params.messages?.[0]?.role).toBe("developer");
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps developer messages for OpenAI reasoning model instructions", async () => {
|
||||
const { compat: _compat, ...baseModel } = getModel("openai", "gpt-5.5")!;
|
||||
const model = { ...baseModel, api: "openai-completions" } as const;
|
||||
@@ -862,6 +889,14 @@ describe("openai-completions tool_choice", () => {
|
||||
expect(params.messages?.[0]?.role).toBe("developer");
|
||||
});
|
||||
|
||||
it("stores OpenRouter Kimi K2.6 reasoning replay compat in built-in metadata", () => {
|
||||
for (const modelId of ["moonshotai/kimi-k2.6", "moonshotai/kimi-k2.6:free"] as const) {
|
||||
const model = getModel("openrouter", modelId)!;
|
||||
expect(model.compat?.supportsDeveloperRole).toBe(false);
|
||||
expect(model.compat?.requiresReasoningContentOnAssistantMessages).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("stores Xiaomi MiMo reasoning replay compat in built-in metadata", () => {
|
||||
const providers = ["xiaomi", "xiaomi-token-plan-cn", "xiaomi-token-plan-ams", "xiaomi-token-plan-sgp"] as const;
|
||||
|
||||
@@ -1260,4 +1295,95 @@ describe("openai-completions tool_choice", () => {
|
||||
expect(params.reasoning).toEqual({ effort: "high" });
|
||||
expect(params.reasoning_effort).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses Ant Ling compatibility metadata", async () => {
|
||||
const model = getModel("ant-ling", "Ring-2.6-1T")!;
|
||||
let payload: unknown;
|
||||
|
||||
expect(model.compat).toMatchObject({
|
||||
supportsStore: false,
|
||||
supportsDeveloperRole: false,
|
||||
supportsReasoningEffort: false,
|
||||
maxTokensField: "max_tokens",
|
||||
thinkingFormat: "ant-ling",
|
||||
supportsLongCacheRetention: false,
|
||||
});
|
||||
expect(model.compat?.supportsStrictMode).toBeUndefined();
|
||||
expect(model.compat?.requiresReasoningContentOnAssistantMessages).toBeUndefined();
|
||||
|
||||
await streamSimple(
|
||||
model,
|
||||
{
|
||||
systemPrompt: "Follow instructions.",
|
||||
messages: [{ role: "user", content: "Hi", timestamp: Date.now() }],
|
||||
},
|
||||
{
|
||||
apiKey: "test",
|
||||
maxTokens: 123,
|
||||
reasoning: "high",
|
||||
cacheRetention: "long",
|
||||
sessionId: "ant-ling-session",
|
||||
onPayload: (params: unknown) => {
|
||||
payload = params;
|
||||
},
|
||||
},
|
||||
).result();
|
||||
|
||||
const params = (payload ?? mockState.lastParams) as {
|
||||
max_tokens?: number;
|
||||
max_completion_tokens?: number;
|
||||
messages?: Array<{ role?: string }>;
|
||||
reasoning?: { effort?: string };
|
||||
reasoning_effort?: string;
|
||||
store?: boolean;
|
||||
prompt_cache_key?: string;
|
||||
prompt_cache_retention?: string;
|
||||
};
|
||||
expect(params.max_tokens).toBe(123);
|
||||
expect(params.max_completion_tokens).toBeUndefined();
|
||||
expect(params.messages?.[0]?.role).toBe("system");
|
||||
expect(params.reasoning).toEqual({ effort: "high" });
|
||||
expect(params.reasoning_effort).toBeUndefined();
|
||||
expect(params.store).toBeUndefined();
|
||||
expect(params.prompt_cache_key).toBeUndefined();
|
||||
expect(params.prompt_cache_retention).toBeUndefined();
|
||||
});
|
||||
|
||||
it("omits Ant Ling reasoning for unmapped direct reasoning efforts and non-reasoning models", async () => {
|
||||
const ring = getModel("ant-ling", "Ring-2.6-1T")!;
|
||||
let payload: unknown;
|
||||
|
||||
await stream(
|
||||
ring,
|
||||
{
|
||||
messages: [{ role: "user", content: "Hi", timestamp: Date.now() }],
|
||||
},
|
||||
{
|
||||
apiKey: "test",
|
||||
reasoningEffort: "medium",
|
||||
onPayload: (params: unknown) => {
|
||||
payload = params;
|
||||
},
|
||||
},
|
||||
).result();
|
||||
|
||||
expect((payload ?? mockState.lastParams) as { reasoning?: unknown }).not.toHaveProperty("reasoning");
|
||||
|
||||
const ling = getModel("ant-ling", "Ling-2.6-flash")!;
|
||||
await streamSimple(
|
||||
ling,
|
||||
{
|
||||
messages: [{ role: "user", content: "Hi", timestamp: Date.now() }],
|
||||
},
|
||||
{
|
||||
apiKey: "test",
|
||||
reasoning: "high",
|
||||
onPayload: (params: unknown) => {
|
||||
payload = params;
|
||||
},
|
||||
},
|
||||
).result();
|
||||
|
||||
expect((payload ?? mockState.lastParams) as { reasoning?: unknown }).not.toHaveProperty("reasoning");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -787,6 +787,30 @@ describe("Generate E2E Tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.NVIDIA_API_KEY)("NVIDIA NIM Provider (Nemotron 3 Super via OpenAI Completions)", () => {
|
||||
const llm = getModel("nvidia", "nvidia/nemotron-3-super-120b-a12b");
|
||||
|
||||
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" });
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.OPENROUTER_API_KEY)("OpenRouter Provider (glm-4.5v via OpenAI Completions)", () => {
|
||||
const llm = getModel("openrouter", "z-ai/glm-4.5v");
|
||||
|
||||
@@ -1145,6 +1169,27 @@ describe("Generate E2E Tests", () => {
|
||||
},
|
||||
);
|
||||
|
||||
describe.skipIf(!process.env.ANT_LING_API_KEY)("Ant Ling Provider (Ling 2.6 Flash via OpenAI Completions)", () => {
|
||||
const llm = getModel("ant-ling", "Ling-2.6-flash");
|
||||
|
||||
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 () => {
|
||||
const ringModel = getModel("ant-ling", "Ring-2.6-1T");
|
||||
await handleThinking(ringModel, { reasoningEffort: "high" });
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// OAuth-based providers (credentials from ~/.pi/agent/oauth.json)
|
||||
// Tokens are resolved at module level (see oauthTokens above)
|
||||
|
||||
@@ -4,14 +4,18 @@
|
||||
|
||||
### Added
|
||||
|
||||
- Added Ant Ling provider selection and setup documentation.
|
||||
- Added NVIDIA NIM provider selection, setup documentation, and direct NIM request attribution headers.
|
||||
- Added `ctx.mode` to extension contexts so extensions can distinguish TUI, RPC, JSON, and print mode.
|
||||
- Added `ctx.getSystemPromptOptions()` for extension commands to inspect the current base system prompt inputs.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed stored XSS in HTML session exports by sanitizing Markdown link and image URLs with a scheme allow-list after stripping control characters.
|
||||
- Fixed SDK embedding in bundled Node apps failing with `ENOENT` when `package.json` is not present next to the bundle entrypoint. The package metadata reader now gracefully handles missing `package.json` by using defaults, enabling `createAgentSession()` without requiring package-adjacent files at runtime ([#5226](https://github.com/earendil-works/pi/issues/5226)).
|
||||
- Fixed HTTP timeout setting not being respected for non-Codex providers (e.g., llama.cpp via OpenAI-compatible API). The `httpIdleTimeoutMs` setting (set via `/settings` HTTP timeout) now applies as the default SDK request timeout for all providers that support it, not just OpenAI Codex Responses. Disabling the timeout (HTTP timeout = false) now correctly disables SDK timeouts for all supported providers by sending a maximum int32 value (effectively infinite) instead of 0, since SDKs treat timeout=0 as an immediate timeout ([#5294](https://github.com/earendil-works/pi/issues/5294)).
|
||||
- Fixed opening and listing very large JSONL session files by reading session entries line-by-line instead of materializing the full file as one string ([#5231](https://github.com/earendil-works/pi/issues/5231)).
|
||||
- Fixed `renderShell: "self"` tool renderers that emit no component lines leaving a blank chat row ([#5299](https://github.com/earendil-works/pi/issues/5299)).
|
||||
|
||||
## [0.78.0] - 2026-05-29
|
||||
|
||||
|
||||
@@ -110,9 +110,11 @@ For each built-in provider, pi maintains a list of tool-capable models, updated
|
||||
|
||||
**API keys:**
|
||||
- Anthropic
|
||||
- Ant Ling
|
||||
- OpenAI
|
||||
- Azure OpenAI
|
||||
- DeepSeek
|
||||
- NVIDIA NIM
|
||||
- Google Gemini
|
||||
- Google Vertex
|
||||
- Amazon Bedrock
|
||||
@@ -296,7 +298,7 @@ Interactive startup asks before loading `.pi` or `.pi.user` in a working directo
|
||||
Pi has two separate startup features:
|
||||
|
||||
- **Update check:** fetches `https://pi.dev/api/latest-version` to check whether a newer Pi version exists. Disable it with `PI_SKIP_VERSION_CHECK=1`. Disabling update checks only turns off this check.
|
||||
- **Install/update telemetry:** after first install or a changelog-detected update, sends an anonymous version ping to `https://pi.dev/api/report-install`. Opt out by setting `enableInstallTelemetry` to `false` in `settings.json`, or by setting `PI_TELEMETRY=0`. This does not disable update checks; Pi may still contact `pi.dev` for the latest version unless update checks are disabled or offline mode is enabled.
|
||||
- **Install/update telemetry:** after first install or a changelog-detected update, sends an anonymous version ping to `https://pi.dev/api/report-install`. This setting also controls optional provider attribution headers for OpenRouter, Cloudflare, and direct NVIDIA NIM requests. Opt out by setting `enableInstallTelemetry` to `false` in `settings.json`, or by setting `PI_TELEMETRY=0`. This does not disable update checks; Pi may still contact `pi.dev` for the latest version unless update checks are disabled or offline mode is enabled.
|
||||
|
||||
Use `--offline` or `PI_OFFLINE=1` to disable all startup network operations described here, including update checks, package update checks, and install/update telemetry.
|
||||
|
||||
@@ -648,7 +650,7 @@ pi --thinking high "Solve this complex problem"
|
||||
| `PI_PACKAGE_DIR` | Override package directory (useful for Nix/Guix where store paths tokenize poorly) |
|
||||
| `PI_OFFLINE` | Disable startup network operations, including update checks, package update checks, and install/update telemetry |
|
||||
| `PI_SKIP_VERSION_CHECK` | Skip the Pi version update check at startup. This prevents the `pi.dev` latest-version request |
|
||||
| `PI_TELEMETRY` | Override install/update telemetry. Use `1`/`true`/`yes` to enable or `0`/`false`/`no` to disable. This does not disable update checks |
|
||||
| `PI_TELEMETRY` | Override install/update telemetry and provider attribution headers. Use `1`/`true`/`yes` to enable or `0`/`false`/`no` to disable. This does not disable update checks |
|
||||
| `PI_CACHE_RETENTION` | Set to `long` for extended prompt cache (Anthropic: 1h, OpenAI: 24h) |
|
||||
| `VISUAL`, `EDITOR` | External editor for Ctrl+G |
|
||||
|
||||
|
||||
@@ -49,9 +49,11 @@ pi
|
||||
| Provider | Environment Variable | `auth.json` key |
|
||||
|----------|----------------------|------------------|
|
||||
| Anthropic | `ANTHROPIC_API_KEY` | `anthropic` |
|
||||
| Ant Ling | `ANT_LING_API_KEY` | `ant-ling` |
|
||||
| Azure OpenAI Responses | `AZURE_OPENAI_API_KEY` | `azure-openai-responses` |
|
||||
| OpenAI | `OPENAI_API_KEY` | `openai` |
|
||||
| DeepSeek | `DEEPSEEK_API_KEY` | `deepseek` |
|
||||
| NVIDIA NIM | `NVIDIA_API_KEY` | `nvidia` |
|
||||
| Google Gemini | `GEMINI_API_KEY` | `google` |
|
||||
| Mistral | `MISTRAL_API_KEY` | `mistral` |
|
||||
| Groq | `GROQ_API_KEY` | `groq` |
|
||||
@@ -84,8 +86,10 @@ Store credentials in `~/.pi/agent/auth.json`:
|
||||
```json
|
||||
{
|
||||
"anthropic": { "type": "api_key", "key": "sk-ant-..." },
|
||||
"ant-ling": { "type": "api_key", "key": "..." },
|
||||
"openai": { "type": "api_key", "key": "sk-..." },
|
||||
"deepseek": { "type": "api_key", "key": "sk-..." },
|
||||
"nvidia": { "type": "api_key", "key": "nvapi-..." },
|
||||
"google": { "type": "api_key", "key": "..." },
|
||||
"opencode": { "type": "api_key", "key": "..." },
|
||||
"opencode-go": { "type": "api_key", "key": "..." },
|
||||
|
||||
@@ -282,7 +282,7 @@ pi --exclude-tools ask_question
|
||||
| `PI_PACKAGE_DIR` | Override package directory, useful for Nix/Guix store paths |
|
||||
| `PI_OFFLINE` | Disable startup network operations, including update checks, package update checks, and install/update telemetry |
|
||||
| `PI_SKIP_VERSION_CHECK` | Skip the Pi version update check at startup. This prevents the `pi.dev` latest-version request |
|
||||
| `PI_TELEMETRY` | Override install/update telemetry: `1`/`true`/`yes` or `0`/`false`/`no`. This does not disable update checks |
|
||||
| `PI_TELEMETRY` | Override install/update telemetry and provider attribution headers: `1`/`true`/`yes` or `0`/`false`/`no`. This does not disable update checks |
|
||||
| `PI_CACHE_RETENTION` | Set to `long` for extended prompt cache where supported |
|
||||
| `VISUAL`, `EDITOR` | External editor for Ctrl+G |
|
||||
|
||||
|
||||
@@ -332,6 +332,7 @@ ${chalk.bold("Examples:")}
|
||||
${chalk.bold("Environment Variables:")}
|
||||
ANTHROPIC_API_KEY - Anthropic Claude API key
|
||||
ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth token (alternative to API key)
|
||||
ANT_LING_API_KEY - Ant Ling API key
|
||||
OPENAI_API_KEY - OpenAI GPT API key
|
||||
AZURE_OPENAI_API_KEY - Azure OpenAI API key
|
||||
AZURE_OPENAI_BASE_URL - Azure OpenAI/Cognitive Services base URL (e.g. https://{resource}.openai.azure.com)
|
||||
@@ -339,6 +340,7 @@ ${chalk.bold("Environment Variables:")}
|
||||
AZURE_OPENAI_API_VERSION - Azure OpenAI API version (default: v1)
|
||||
AZURE_OPENAI_DEPLOYMENT_NAME_MAP - Azure OpenAI model=deployment map (comma-separated)
|
||||
DEEPSEEK_API_KEY - DeepSeek API key
|
||||
NVIDIA_API_KEY - NVIDIA NIM API key
|
||||
GEMINI_API_KEY - Google Gemini API key
|
||||
GROQ_API_KEY - Groq API key
|
||||
CEREBRAS_API_KEY - Cerebras API key
|
||||
|
||||
@@ -2787,6 +2787,7 @@ export class AgentSession {
|
||||
customInstructions,
|
||||
replaceInstructions,
|
||||
reserveTokens: branchSummarySettings.reserveTokens,
|
||||
streamFn: this.agent.streamFn,
|
||||
});
|
||||
if (result.aborted) {
|
||||
return { cancelled: true, aborted: true };
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* a summary of the branch being left so context isn't lost.
|
||||
*/
|
||||
|
||||
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
||||
import type { Model } from "@earendil-works/pi-ai";
|
||||
import type { AgentMessage, StreamFn } from "@earendil-works/pi-agent-core";
|
||||
import type { Model, SimpleStreamOptions } from "@earendil-works/pi-ai";
|
||||
import { completeSimple } from "@earendil-works/pi-ai";
|
||||
import {
|
||||
convertToLlm,
|
||||
@@ -77,6 +77,8 @@ export interface GenerateBranchSummaryOptions {
|
||||
replaceInstructions?: boolean;
|
||||
/** Tokens reserved for prompt + LLM response (default 16384) */
|
||||
reserveTokens?: number;
|
||||
/** Optional session stream function. Used to preserve SDK request behavior without mutating agent state. */
|
||||
streamFn?: StreamFn;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -284,7 +286,16 @@ export async function generateBranchSummary(
|
||||
entries: SessionEntry[],
|
||||
options: GenerateBranchSummaryOptions,
|
||||
): Promise<BranchSummaryResult> {
|
||||
const { model, apiKey, headers, signal, customInstructions, replaceInstructions, reserveTokens = 16384 } = options;
|
||||
const {
|
||||
model,
|
||||
apiKey,
|
||||
headers,
|
||||
signal,
|
||||
customInstructions,
|
||||
replaceInstructions,
|
||||
reserveTokens = 16384,
|
||||
streamFn,
|
||||
} = options;
|
||||
|
||||
// Token budget = context window minus reserved space for prompt + response
|
||||
const contextWindow = model.contextWindow || 128000;
|
||||
@@ -320,12 +331,14 @@ export async function generateBranchSummary(
|
||||
},
|
||||
];
|
||||
|
||||
// Call LLM for summarization
|
||||
const response = await completeSimple(
|
||||
model,
|
||||
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
|
||||
{ apiKey, headers, signal, maxTokens: 2048 },
|
||||
);
|
||||
// Call LLM for summarization. Prefer the session stream function so SDK
|
||||
// request behavior (timeouts, retries, attribution headers) stays consistent
|
||||
// without running through agent state/events.
|
||||
const context = { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages };
|
||||
const requestOptions: SimpleStreamOptions = { apiKey, headers, signal, maxTokens: 2048 };
|
||||
const response = streamFn
|
||||
? await (await streamFn(model, context, requestOptions)).result()
|
||||
: await completeSimple(model, context, requestOptions);
|
||||
|
||||
// Check if aborted or errored
|
||||
if (response.stopReason === "aborted") {
|
||||
|
||||
@@ -613,6 +613,18 @@
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function sanitizeMarkdownUrl(value) {
|
||||
const href = String(value || '').trim().replace(/[\x00-\x1f\x7f]/g, '');
|
||||
if (!href) return href;
|
||||
|
||||
const scheme = href.match(/^([A-Za-z][A-Za-z0-9+.-]*):/);
|
||||
if (scheme && !/^(https?|mailto|tel|ftp)$/i.test(scheme[1])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate string to maxLen chars, append "..." if truncated.
|
||||
*/
|
||||
@@ -1569,10 +1581,11 @@
|
||||
}
|
||||
},
|
||||
renderer: {
|
||||
// Sanitize link URLs to prevent javascript:/vbscript:/data: XSS
|
||||
// Sanitize link URLs with a scheme allow-list. Browsers strip C0
|
||||
// controls from schemes, so strip them before checking and emitting.
|
||||
link(token) {
|
||||
const href = (token.href || '').trim();
|
||||
if (/^\s*(javascript|vbscript|data):/i.test(href)) {
|
||||
const href = sanitizeMarkdownUrl(token.href);
|
||||
if (href === null) {
|
||||
return this.parser.parseInline(token.tokens);
|
||||
}
|
||||
let out = '<a href="' + escapeHtml(href) + '"';
|
||||
@@ -1582,10 +1595,10 @@
|
||||
out += '>' + this.parser.parseInline(token.tokens) + '</a>';
|
||||
return out;
|
||||
},
|
||||
// Sanitize image src URLs
|
||||
// Sanitize image src URLs with the same scheme allow-list.
|
||||
image(token) {
|
||||
const href = (token.href || '').trim();
|
||||
if (/^\s*(javascript|vbscript|data):/i.test(href)) {
|
||||
const href = sanitizeMarkdownUrl(token.href);
|
||||
if (href === null) {
|
||||
return escapeHtml(token.text || '');
|
||||
}
|
||||
let out = '<img src="' + escapeHtml(href) + '" alt="' + escapeHtml(token.text || '') + '"';
|
||||
|
||||
@@ -13,10 +13,12 @@ import type { ModelRegistry } from "./model-registry.ts";
|
||||
/** Default model IDs for each known provider */
|
||||
export const defaultModelPerProvider: Record<KnownProvider, string> = {
|
||||
"amazon-bedrock": "us.anthropic.claude-opus-4-6-v1",
|
||||
"ant-ling": "Ring-2.6-1T",
|
||||
anthropic: "claude-opus-4-8",
|
||||
openai: "gpt-5.4",
|
||||
"azure-openai-responses": "gpt-5.4",
|
||||
"openai-codex": "gpt-5.5",
|
||||
nvidia: "nvidia/nemotron-3-super-120b-a12b",
|
||||
deepseek: "deepseek-v4-pro",
|
||||
google: "gemini-3.1-pro-preview",
|
||||
"google-vertex": "gemini-3.1-pro-preview",
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { Api, Model } from "@earendil-works/pi-ai";
|
||||
import type { SettingsManager } from "./settings-manager.ts";
|
||||
import { isInstallTelemetryEnabled } from "./telemetry.ts";
|
||||
|
||||
const OPENROUTER_HOST = "openrouter.ai";
|
||||
const NVIDIA_NIM_HOST = "integrate.api.nvidia.com";
|
||||
const CLOUDFLARE_API_HOST = "api.cloudflare.com";
|
||||
const CLOUDFLARE_AI_GATEWAY_HOST = "gateway.ai.cloudflare.com";
|
||||
const OPENCODE_HOST = "opencode.ai";
|
||||
|
||||
function matchesHost(baseUrl: string, expectedHost: string): boolean {
|
||||
try {
|
||||
return new URL(baseUrl).hostname === expectedHost;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isOpenRouterModel(model: Model<Api>): boolean {
|
||||
return model.provider === "openrouter" || model.baseUrl.includes(OPENROUTER_HOST);
|
||||
}
|
||||
|
||||
function isNvidiaNimModel(model: Model<Api>): boolean {
|
||||
return model.provider === "nvidia" || matchesHost(model.baseUrl, NVIDIA_NIM_HOST);
|
||||
}
|
||||
|
||||
function isCloudflareModel(model: Model<Api>): boolean {
|
||||
return (
|
||||
model.provider === "cloudflare-workers-ai" ||
|
||||
model.provider === "cloudflare-ai-gateway" ||
|
||||
matchesHost(model.baseUrl, CLOUDFLARE_API_HOST) ||
|
||||
matchesHost(model.baseUrl, CLOUDFLARE_AI_GATEWAY_HOST)
|
||||
);
|
||||
}
|
||||
|
||||
function getDefaultAttributionHeaders(
|
||||
model: Model<Api>,
|
||||
settingsManager: SettingsManager,
|
||||
): Record<string, string> | undefined {
|
||||
if (!isInstallTelemetryEnabled(settingsManager)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isOpenRouterModel(model)) {
|
||||
return {
|
||||
"HTTP-Referer": "https://pi.dev",
|
||||
"X-OpenRouter-Title": "pi",
|
||||
"X-OpenRouter-Categories": "cli-agent",
|
||||
};
|
||||
}
|
||||
|
||||
if (isNvidiaNimModel(model)) {
|
||||
return {
|
||||
"X-BILLING-INVOKE-ORIGIN": "Pi",
|
||||
};
|
||||
}
|
||||
|
||||
if (isCloudflareModel(model)) {
|
||||
return {
|
||||
"User-Agent": "pi-coding-agent",
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getSessionHeaders(model: Model<Api>, sessionId: string | undefined): Record<string, string> | undefined {
|
||||
if (!sessionId) return undefined;
|
||||
if (
|
||||
model.provider !== "opencode" &&
|
||||
model.provider !== "opencode-go" &&
|
||||
!matchesHost(model.baseUrl, OPENCODE_HOST)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return { "x-opencode-session": sessionId, "x-opencode-client": "pi" };
|
||||
}
|
||||
|
||||
export function mergeProviderAttributionHeaders(
|
||||
model: Model<Api>,
|
||||
settingsManager: SettingsManager,
|
||||
sessionId: string | undefined,
|
||||
...headerSources: Array<Record<string, string> | undefined>
|
||||
): Record<string, string> | undefined {
|
||||
const merged = {
|
||||
...getSessionHeaders(model, sessionId),
|
||||
...getDefaultAttributionHeaders(model, settingsManager),
|
||||
};
|
||||
|
||||
for (const headers of headerSources) {
|
||||
if (headers) {
|
||||
Object.assign(merged, headers);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(merged).length > 0 ? merged : undefined;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export const BUILT_IN_PROVIDER_DISPLAY_NAMES: Record<string, string> = {
|
||||
anthropic: "Anthropic",
|
||||
"amazon-bedrock": "Amazon Bedrock",
|
||||
"ant-ling": "Ant Ling",
|
||||
"azure-openai-responses": "Azure OpenAI Responses",
|
||||
cerebras: "Cerebras",
|
||||
"cloudflare-ai-gateway": "Cloudflare AI Gateway",
|
||||
@@ -17,6 +18,7 @@ export const BUILT_IN_PROVIDER_DISPLAY_NAMES: Record<string, string> = {
|
||||
"minimax-cn": "MiniMax (China)",
|
||||
moonshotai: "Moonshot AI",
|
||||
"moonshotai-cn": "Moonshot AI (China)",
|
||||
nvidia: "NVIDIA NIM",
|
||||
opencode: "OpenCode Zen",
|
||||
"opencode-go": "OpenCode Go",
|
||||
openai: "OpenAI",
|
||||
|
||||
@@ -11,11 +11,11 @@ import type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefi
|
||||
import { convertToLlm } from "./messages.ts";
|
||||
import { ModelRegistry } from "./model-registry.ts";
|
||||
import { findInitialModel } from "./model-resolver.ts";
|
||||
import { mergeProviderAttributionHeaders } from "./provider-attribution.ts";
|
||||
import type { ResourceLoader } from "./resource-loader.ts";
|
||||
import { DefaultResourceLoader } from "./resource-loader.ts";
|
||||
import { getDefaultSessionDir, SessionManager } from "./session-manager.ts";
|
||||
import { SettingsManager } from "./settings-manager.ts";
|
||||
import { isInstallTelemetryEnabled } from "./telemetry.ts";
|
||||
import { time } from "./timings.ts";
|
||||
import {
|
||||
createBashTool,
|
||||
@@ -128,44 +128,6 @@ function getDefaultAgentDir(): string {
|
||||
return getAgentDir();
|
||||
}
|
||||
|
||||
function getAttributionHeaders(
|
||||
model: Model<any>,
|
||||
settingsManager: SettingsManager,
|
||||
sessionId?: string,
|
||||
): Record<string, string> | undefined {
|
||||
if (
|
||||
sessionId &&
|
||||
(model.provider === "opencode" || model.provider === "opencode-go" || model.baseUrl.includes("opencode.ai"))
|
||||
) {
|
||||
return { "x-opencode-session": sessionId, "x-opencode-client": "pi" };
|
||||
}
|
||||
|
||||
if (!isInstallTelemetryEnabled(settingsManager)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (model.provider === "openrouter" || model.baseUrl.includes("openrouter.ai")) {
|
||||
return {
|
||||
"HTTP-Referer": "https://pi.dev",
|
||||
"X-OpenRouter-Title": "pi",
|
||||
"X-OpenRouter-Categories": "cli-agent",
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
model.provider === "cloudflare-workers-ai" ||
|
||||
model.provider === "cloudflare-ai-gateway" ||
|
||||
model.baseUrl.includes("api.cloudflare.com") ||
|
||||
model.baseUrl.includes("gateway.ai.cloudflare.com")
|
||||
) {
|
||||
return {
|
||||
"User-Agent": "pi-coding-agent",
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an AgentSession with the specified options.
|
||||
*
|
||||
@@ -349,7 +311,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
const timeoutMs = options?.timeoutMs ?? providerRetrySettings.timeoutMs ?? effectiveTimeoutMs;
|
||||
const websocketConnectTimeoutMs =
|
||||
options?.websocketConnectTimeoutMs ?? settingsManager.getWebSocketConnectTimeoutMs();
|
||||
const attributionHeaders = getAttributionHeaders(model, settingsManager, options?.sessionId);
|
||||
return streamSimple(model, context, {
|
||||
...options,
|
||||
apiKey: auth.apiKey,
|
||||
@@ -357,10 +318,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
websocketConnectTimeoutMs,
|
||||
maxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,
|
||||
maxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,
|
||||
headers:
|
||||
attributionHeaders || auth.headers || options?.headers
|
||||
? { ...attributionHeaders, ...auth.headers, ...options?.headers }
|
||||
: undefined,
|
||||
headers: mergeProviderAttributionHeaders(
|
||||
model,
|
||||
settingsManager,
|
||||
options?.sessionId,
|
||||
auth.headers,
|
||||
options?.headers,
|
||||
),
|
||||
});
|
||||
},
|
||||
onPayload: async (payload, _model) => {
|
||||
|
||||
@@ -222,6 +222,31 @@ export class ToolExecutionComponent extends Container {
|
||||
if (this.hideComponent) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (this.hasRendererDefinition() && this.getRenderShell() === "self") {
|
||||
const contentLines = this.selfRenderContainer.render(width);
|
||||
if (contentLines.length === 0 && this.imageComponents.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
if (contentLines.length > 0) {
|
||||
lines.push("");
|
||||
lines.push(...contentLines);
|
||||
}
|
||||
for (let i = 0; i < this.imageComponents.length; i++) {
|
||||
const spacer = this.imageSpacers[i];
|
||||
if (spacer) {
|
||||
lines.push(...spacer.render(width));
|
||||
}
|
||||
const imageComponent = this.imageComponents[i];
|
||||
if (imageComponent) {
|
||||
lines.push(...imageComponent.render(width));
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
return super.render(width);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,15 +4,20 @@ import { describe, expect, it } from "vitest";
|
||||
describe("export HTML markdown link sanitization", () => {
|
||||
const templateJs = readFileSync(new URL("../src/core/export-html/template.js", import.meta.url), "utf-8");
|
||||
|
||||
it("overrides the marked link renderer to block javascript: protocol", () => {
|
||||
// The custom link renderer must check for dangerous protocols
|
||||
it("overrides the marked link renderer to use scheme allow-list sanitization", () => {
|
||||
expect(templateJs).toMatch(/link\s*\(\s*token\s*\)/);
|
||||
expect(templateJs).toMatch(/javascript/i);
|
||||
expect(templateJs).toMatch(/vbscript/i);
|
||||
expect(templateJs).toMatch(/sanitizeMarkdownUrl\(token\.href\)/);
|
||||
expect(templateJs).toMatch(/\^\(https\?\|mailto\|tel\|ftp\)/);
|
||||
});
|
||||
|
||||
it("overrides the marked image renderer to block javascript: protocol", () => {
|
||||
it("overrides the marked image renderer to use scheme allow-list sanitization", () => {
|
||||
expect(templateJs).toMatch(/image\s*\(\s*token\s*\)/);
|
||||
expect(templateJs).toMatch(/sanitizeMarkdownUrl\(token\.href\)/);
|
||||
});
|
||||
|
||||
it("strips C0 controls before checking and emitting markdown URLs", () => {
|
||||
expect(templateJs).toContain("replace(/[\\x00-\\x1f\\x7f]/g, '')");
|
||||
expect(templateJs).not.toMatch(/\^\\s\*\(javascript\|vbscript\|data\):/i);
|
||||
});
|
||||
|
||||
it("escapes href attributes in the custom link renderer", () => {
|
||||
|
||||
@@ -378,11 +378,12 @@ describe("default model selection", () => {
|
||||
expect(defaultModelPerProvider["openai-codex"]).toBe("gpt-5.5");
|
||||
});
|
||||
|
||||
test("zai, minimax, and cerebras defaults track current models", () => {
|
||||
test("zai, minimax, cerebras, and ant-ling defaults track current models", () => {
|
||||
expect(defaultModelPerProvider.zai).toBe("glm-5.1");
|
||||
expect(defaultModelPerProvider.minimax).toBe("MiniMax-M2.7");
|
||||
expect(defaultModelPerProvider["minimax-cn"]).toBe("MiniMax-M2.7");
|
||||
expect(defaultModelPerProvider.cerebras).toBe("zai-glm-4.7");
|
||||
expect(defaultModelPerProvider["ant-ling"]).toBe("Ring-2.6-1T");
|
||||
});
|
||||
|
||||
test("ai-gateway default tracks current model", () => {
|
||||
|
||||
@@ -15,14 +15,14 @@ import { createAgentSession } from "../src/core/sdk.ts";
|
||||
import { SessionManager } from "../src/core/session-manager.ts";
|
||||
import { SettingsManager } from "../src/core/settings-manager.ts";
|
||||
|
||||
describe("createAgentSession OpenRouter attribution headers", () => {
|
||||
describe("createAgentSession provider attribution headers", () => {
|
||||
let tempDir: string;
|
||||
let cwd: string;
|
||||
let agentDir: string;
|
||||
let originalTelemetryEnv: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = join(tmpdir(), `pi-sdk-openrouter-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
tempDir = join(tmpdir(), `pi-sdk-attribution-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
cwd = join(tempDir, "project");
|
||||
agentDir = join(tempDir, "agent");
|
||||
mkdirSync(cwd, { recursive: true });
|
||||
@@ -42,9 +42,9 @@ describe("createAgentSession OpenRouter attribution headers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
function createModel(provider: string, baseUrl: string): Model<Api> {
|
||||
function createModel(provider: string, baseUrl: string, id = `${provider}-test-model`): Model<Api> {
|
||||
return {
|
||||
id: `${provider}-test-model`,
|
||||
id,
|
||||
name: `${provider} Test Model`,
|
||||
api: "openai-completions",
|
||||
provider,
|
||||
@@ -172,6 +172,14 @@ describe("createAgentSession OpenRouter attribution headers", () => {
|
||||
expect(headers?.["X-OpenRouter-Categories"]).toBe("cli-agent");
|
||||
});
|
||||
|
||||
it("preserves legacy OpenRouter base URL substring attribution matching", async () => {
|
||||
const headers = await captureHeaders(createModel("custom-openrouter", "not-a-url-openrouter.ai"));
|
||||
|
||||
expect(headers?.["HTTP-Referer"]).toBe("https://pi.dev");
|
||||
expect(headers?.["X-OpenRouter-Title"]).toBe("pi");
|
||||
expect(headers?.["X-OpenRouter-Categories"]).toBe("cli-agent");
|
||||
});
|
||||
|
||||
it("lets provider and request headers override the defaults", async () => {
|
||||
const headers = await captureHeaders(createModel("openrouter", "https://openrouter.ai/api/v1"), {
|
||||
providerHeaders: {
|
||||
@@ -188,6 +196,56 @@ describe("createAgentSession OpenRouter attribution headers", () => {
|
||||
expect(headers?.["X-OpenRouter-Categories"]).toBe("provider-category");
|
||||
});
|
||||
|
||||
it("adds default attribution headers for direct NVIDIA NIM endpoints", async () => {
|
||||
const headers = await captureHeaders(createModel("custom-nim", "https://integrate.api.nvidia.com/v1"));
|
||||
|
||||
expect(headers?.["X-BILLING-INVOKE-ORIGIN"]).toBe("Pi");
|
||||
});
|
||||
|
||||
it("adds default attribution headers for the NVIDIA provider", async () => {
|
||||
const headers = await captureHeaders(createModel("nvidia", "https://example.test/v1"));
|
||||
|
||||
expect(headers?.["X-BILLING-INVOKE-ORIGIN"]).toBe("Pi");
|
||||
});
|
||||
|
||||
it("does not add NVIDIA NIM attribution headers when telemetry is disabled", async () => {
|
||||
const headers = await captureHeaders(createModel("nvidia", "https://integrate.api.nvidia.com/v1"), {
|
||||
telemetryEnabled: false,
|
||||
});
|
||||
|
||||
expect(headers?.["X-BILLING-INVOKE-ORIGIN"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("lets provider and request headers override NVIDIA NIM defaults", async () => {
|
||||
const headers = await captureHeaders(createModel("nvidia", "https://integrate.api.nvidia.com/v1"), {
|
||||
providerHeaders: {
|
||||
"X-BILLING-INVOKE-ORIGIN": "Provider",
|
||||
},
|
||||
requestHeaders: {
|
||||
"X-BILLING-INVOKE-ORIGIN": "Request",
|
||||
},
|
||||
});
|
||||
|
||||
expect(headers?.["X-BILLING-INVOKE-ORIGIN"]).toBe("Request");
|
||||
});
|
||||
|
||||
it("does not add NVIDIA NIM attribution headers for NVIDIA models routed through OpenRouter", async () => {
|
||||
const headers = await captureHeaders(
|
||||
createModel("openrouter", "https://openrouter.ai/api/v1", "nvidia/nemotron-3-super-120b-a12b"),
|
||||
);
|
||||
|
||||
expect(headers?.["HTTP-Referer"]).toBe("https://pi.dev");
|
||||
expect(headers?.["X-BILLING-INVOKE-ORIGIN"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not add NVIDIA NIM attribution headers for NVIDIA models routed through Vercel AI Gateway", async () => {
|
||||
const headers = await captureHeaders(
|
||||
createModel("vercel-ai-gateway", "https://ai-gateway.vercel.sh/v1", "nvidia/nemotron-3-super-120b-a12b"),
|
||||
);
|
||||
|
||||
expect(headers?.["X-BILLING-INVOKE-ORIGIN"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("adds OpenCode session headers", async () => {
|
||||
const headers = await captureHeaders(createModel("opencode", "https://opencode.ai/zen/v1"), {
|
||||
sessionId: "opencode-session",
|
||||
|
||||
@@ -119,10 +119,10 @@ describe("createAgentSession stream options", () => {
|
||||
expect(options?.timeoutMs).toBe(1234);
|
||||
});
|
||||
|
||||
it("does not default timeoutMs from httpIdleTimeoutMs for other providers", async () => {
|
||||
it("defaults timeoutMs from httpIdleTimeoutMs for all providers", async () => {
|
||||
const options = await captureStreamOptions("openai-completions", { httpIdleTimeoutMs: 1234 });
|
||||
|
||||
expect(options?.timeoutMs).toBeUndefined();
|
||||
expect(options?.timeoutMs).toBe(1234);
|
||||
});
|
||||
|
||||
it("lets request timeoutMs override httpIdleTimeoutMs for OpenAI Codex", async () => {
|
||||
|
||||
@@ -67,6 +67,37 @@ describe("ToolExecutionComponent parity", () => {
|
||||
expect(rendered).toContain("custom result");
|
||||
});
|
||||
|
||||
test("self-rendered empty tool rows take no layout space", () => {
|
||||
const toolDefinition: ToolDefinition = {
|
||||
...createBaseToolDefinition(),
|
||||
renderShell: "self",
|
||||
renderCall: () => new Text("", 0, 0),
|
||||
renderResult: () => new Text("", 0, 0),
|
||||
};
|
||||
|
||||
const component = new ToolExecutionComponent(
|
||||
"custom_tool",
|
||||
"tool-empty-self-render",
|
||||
{},
|
||||
{},
|
||||
toolDefinition,
|
||||
createFakeTui(),
|
||||
process.cwd(),
|
||||
);
|
||||
expect(component.render(120)).toEqual([]);
|
||||
|
||||
component.updateResult(
|
||||
{
|
||||
content: [],
|
||||
details: {},
|
||||
isError: false,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
expect(component.render(120)).toEqual([]);
|
||||
});
|
||||
|
||||
test("uses built-in rendering for built-in overrides without custom renderers", () => {
|
||||
const overrideDefinition: ToolDefinition = {
|
||||
...createBaseToolDefinition("edit"),
|
||||
|
||||
Reference in New Issue
Block a user