feat(ai): add GPT-6.1 Terra

Add gpt-6.1-terra to the OpenAI, Azure OpenAI Responses, and OpenAI
Codex providers with official pricing ($1 input, $0.05 cached input,
$1.25 cache writes, $5 output, long-context tier above 272k), tool
search, additional tools, and mid-conversation effort changes, as for
GPT-6.1 Sol. Unlike Sol it accepts reasoning.effort "none", so off is
supported; both reject "minimal" (verified against Codex). Codex serves
it with a 500K context window.
This commit is contained in:
Armin Ronacher 2026-10-26 21:27:08 +01:00
parent 89c921d8b5
commit 363ad78bf0
5 changed files with 68 additions and 13 deletions

View file

@ -10,6 +10,7 @@
- Added optional `openBrowserOnConfirm` and `copyCodeOnConfirm` to `device_code` auth events and `OAuthDeviceCodeInfo`. Both default to `true`; set them to `false` to stop clients from offering to open the verification page or copy the code ([#9282](https://github.com/earendil-works/pi/issues/9282), [#9301](https://github.com/earendil-works/pi/pull/9301))
- Added `StreamingJsonParser`, an incremental, tolerant parser for JSON text that arrives in pieces (each piece is read once; snapshots of the partial value never change once returned), and `ToolCallArgumentsStream`, which every provider now uses for streamed tool-call arguments; its `append()` also returns the changes each piece made (`JsonChange`: a value set at a path, or text appended to a string), for writers that keep their own copy ([#9265](https://github.com/earendil-works/pi/issues/9265))
- Added GPT-6.1 Terra to the OpenAI, Azure OpenAI Responses, and OpenAI Codex providers with official pricing ($1 input, $0.05 cached input, $1.25 cache writes, $5 output, long-context tier above 272k), tool search, and mid-conversation effort changes. Unlike GPT-6.1 Sol it supports `off` (reasoning effort `none`); Codex serves it with a 500K context window
### Changed

View file

@ -378,6 +378,7 @@ const OPENAI_TOOL_SEARCH_MODEL_IDS = new Set([
"gpt-6-sol",
"gpt-6-luna",
"gpt-6.1-sol",
"gpt-6.1-terra",
]);
const OPENAI_ADDITIONAL_TOOLS_MODEL_IDS = OPENAI_TOOL_SEARCH_MODEL_IDS;
const OPENAI_MID_CONVO_SYSTEM_MESSAGE_MODEL_IDS = OPENAI_TOOL_SEARCH_MODEL_IDS;
@ -389,10 +390,17 @@ const OPENAI_CODEX_ADDITIONAL_TOOLS_MODEL_IDS = new Set([
"gpt-6-sol",
"gpt-6-luna",
"gpt-6.1-sol",
"gpt-6.1-terra",
]);
// OpenAI documents configuration_update for the GPT-6 family.
// https://developers.openai.com/api/docs/guides/reasoning
const OPENAI_MID_CONVO_EFFORT_MODEL_IDS = new Set(["gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-6.1-sol"]);
const OPENAI_MID_CONVO_EFFORT_MODEL_IDS = new Set([
"gpt-6-astra",
"gpt-6-sol",
"gpt-6-luna",
"gpt-6.1-sol",
"gpt-6.1-terra",
]);
const OPENAI_LONG_CONTEXT_INPUT_THRESHOLD = 272000;
const OPENAI_SHORT_CONTEXT_CAPPED_MODEL_IDS = new Set([
"gpt-5.4",
@ -404,6 +412,7 @@ const OPENAI_SHORT_CONTEXT_CAPPED_MODEL_IDS = new Set([
"gpt-6-sol",
"gpt-6-luna",
"gpt-6.1-sol",
"gpt-6.1-terra",
]);
const OPENAI_LONG_CONTEXT_PRICING_MODEL_IDS = new Set([
"gpt-5.4",
@ -417,6 +426,7 @@ const OPENAI_LONG_CONTEXT_PRICING_MODEL_IDS = new Set([
"gpt-6-sol",
"gpt-6-luna",
"gpt-6.1-sol",
"gpt-6.1-terra",
]);
// Keep the generated default no less restrictive than coding-agent's historical
@ -455,6 +465,7 @@ const OPENAI_STANDARD_COSTS: Record<string, ModelCost> = {
"gpt-6-luna": { input: 0.1, output: 0.5, cacheRead: 0.01, cacheWrite: 0.125 },
"gpt-6-sol": { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
"gpt-6.1-sol": { input: 2, output: 10, cacheRead: 0.1, cacheWrite: 2.5 },
"gpt-6.1-terra": { input: 1, output: 5, cacheRead: 0.05, cacheWrite: 1.25 },
};
const OPENAI_RESPONSES_NONE_REASONING_MODELS = new Set([
@ -1072,13 +1083,15 @@ function applyThinkingLevelMetadata(model: Model<any>): void {
(model.id === "gpt-6-astra" ||
model.id === "gpt-6-sol" ||
model.id === "gpt-6-luna" ||
model.id === "gpt-6.1-sol") &&
model.id === "gpt-6.1-sol" ||
model.id === "gpt-6.1-terra") &&
(model.api === "openai-responses" ||
model.api === "azure-openai-responses" ||
model.api === "openai-codex-responses")
) {
mergeThinkingLevelMap(model, {
// GPT-6 Astra and GPT-6.1 Sol reject reasoning.effort "none".
// GPT-6 Astra and GPT-6.1 Sol reject reasoning.effort "none"; GPT-6.1 Terra accepts it. All reject
// "minimal" (verified on Codex, October 2026).
off: model.id === "gpt-6-astra" || model.id === "gpt-6.1-sol" ? null : "none",
minimal: null,
low: "low",
@ -2957,6 +2970,18 @@ async function generateModels() {
contextWindow: OPENAI_LONG_CONTEXT_INPUT_THRESHOLD,
maxTokens: 128000,
},
{
id: "gpt-6.1-terra",
name: "GPT-6.1 Terra",
api: "openai-responses",
baseUrl: "https://api.openai.com/v1",
provider: "openai",
reasoning: true,
input: ["text", "image"],
cost: withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS["gpt-6.1-terra"]),
contextWindow: OPENAI_LONG_CONTEXT_INPUT_THRESHOLD,
maxTokens: 128000,
},
{
id: "gpt-6-astra",
name: "GPT-6 Astra",
@ -3177,7 +3202,7 @@ async function generateModels() {
// GPT-6.1 Sol uses the 500k default returned by /backend-api/codex/models.
const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
const CODEX_CONTEXT = 272000;
const CODEX_GPT_61_SOL_CONTEXT = 500000;
const CODEX_GPT_61_CONTEXT = 500000;
const CODEX_GPT_56_CONTEXT = 272000;
const CODEX_MAX_TOKENS = 128000;
const codexModels: Model<"openai-codex-responses">[] = [
@ -3190,7 +3215,19 @@ async function generateModels() {
reasoning: true,
input: ["text", "image"],
cost: withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS["gpt-6.1-sol"]),
contextWindow: CODEX_GPT_61_SOL_CONTEXT,
contextWindow: CODEX_GPT_61_CONTEXT,
maxTokens: CODEX_MAX_TOKENS,
},
{
id: "gpt-6.1-terra",
name: "GPT-6.1 Terra",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: CODEX_BASE_URL,
reasoning: true,
input: ["text", "image"],
cost: withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS["gpt-6.1-terra"]),
contextWindow: CODEX_GPT_61_CONTEXT,
maxTokens: CODEX_MAX_TOKENS,
},
{

View file

@ -240,12 +240,17 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => {
});
describe("OpenAI Responses Provider", () => {
it.each(["gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-6-astra", "gpt-6-luna", "gpt-6-sol"] as const)(
"does not enable cache warming from the documented TTL alone for %s",
(modelId) => {
expect(getModel("openai", modelId).promptCache).toBeUndefined();
},
);
it.each([
"gpt-5.6-luna",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-6-astra",
"gpt-6-luna",
"gpt-6-sol",
"gpt-6.1-terra",
] as const)("does not enable cache warming from the documented TTL alone for %s", (modelId) => {
expect(getModel("openai", modelId).promptCache).toBeUndefined();
});
it.skipIf(!process.env.OPENAI_API_KEY)(
"should not set prompt_cache_retention when PI_CACHE_RETENTION is not set",

View file

@ -67,7 +67,7 @@ describe("max thinking level", () => {
expect(clampThinkingLevel(model, "xhigh")).toBe("max");
});
it.each(["gpt-5.6-sol", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-6.1-sol"] as const)(
it.each(["gpt-5.6-sol", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-6.1-sol", "gpt-6.1-terra"] as const)(
"sends max to the Codex Responses API for %s",
async (modelId) => {
const model = getModel("openai-codex", modelId)!;

View file

@ -93,13 +93,14 @@ describe("getSupportedThinkingLevels", () => {
"gpt-6-sol",
"gpt-6-luna",
"gpt-6.1-sol",
"gpt-6.1-terra",
] as const)("includes xhigh for openai-codex %s models", (modelId) => {
const model = getModel("openai-codex", modelId);
expect(model).toBeDefined();
expect(getSupportedThinkingLevels(model!)).toContain("xhigh");
});
it.each(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-sol", "gpt-6-luna"] as const)(
it.each(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-sol", "gpt-6-luna", "gpt-6.1-terra"] as const)(
"includes xhigh and max for OpenAI %s models",
(modelId) => {
const model = getModel("openai", modelId);
@ -123,11 +124,22 @@ describe("getSupportedThinkingLevels", () => {
}
});
// GPT-6.1 Terra accepts reasoning.effort "none" but, like GPT-6.1 Sol, rejects "minimal".
it("supports off but not minimal for GPT-6.1 Terra", () => {
for (const provider of ["openai", "azure", "openai-codex"] as const) {
const model = getModel(provider, "gpt-6.1-terra");
expect(model).toBeDefined();
expect(model!.thinkingLevelMap?.off).toBe("none");
expect(model!.thinkingLevelMap?.minimal).toBe(provider === "openai-codex" ? "low" : null);
}
});
it.each([
["gpt-6-sol", { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 }, 272000],
["gpt-6-luna", { input: 0.1, output: 0.5, cacheRead: 0.01, cacheWrite: 0.125 }, 272000],
// Codex serves GPT-6.1 Sol with a 500K default context window (#10840)
["gpt-6.1-sol", { input: 2, output: 10, cacheRead: 0.1, cacheWrite: 2.5 }, 500000],
["gpt-6.1-terra", { input: 1, output: 5, cacheRead: 0.05, cacheWrite: 1.25 }, 500000],
] as const)("includes official metadata for OpenAI and Codex %s", (modelId, cost, codexContextWindow) => {
for (const provider of ["openai", "openai-codex"] as const) {
const model = getModel(provider, modelId);