From 363ad78bf0625e1dfbe848429782f5e07bb1584e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Oct 2026 21:27:08 +0100 Subject: [PATCH] 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. --- packages/ai/CHANGELOG.md | 1 + packages/ai/scripts/generate-models.ts | 47 +++++++++++++++++++++--- packages/ai/test/cache-retention.test.ts | 17 ++++++--- packages/ai/test/max-thinking.test.ts | 2 +- packages/ai/test/supports-xhigh.test.ts | 14 ++++++- 5 files changed, 68 insertions(+), 13 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 244d96f74..882ce5609 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -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 diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index e9027559e..96d324653 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -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 = { "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): 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, }, { diff --git a/packages/ai/test/cache-retention.test.ts b/packages/ai/test/cache-retention.test.ts index 8e06cea93..89ce3d8f6 100644 --- a/packages/ai/test/cache-retention.test.ts +++ b/packages/ai/test/cache-retention.test.ts @@ -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", diff --git a/packages/ai/test/max-thinking.test.ts b/packages/ai/test/max-thinking.test.ts index 653d9a820..bc76d0d08 100644 --- a/packages/ai/test/max-thinking.test.ts +++ b/packages/ai/test/max-thinking.test.ts @@ -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)!; diff --git a/packages/ai/test/supports-xhigh.test.ts b/packages/ai/test/supports-xhigh.test.ts index d16fdb61d..91f5c72f0 100644 --- a/packages/ai/test/supports-xhigh.test.ts +++ b/packages/ai/test/supports-xhigh.test.ts @@ -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);