diff --git a/packages/ai-config/src/model-capabilities/__tests__/infer.test.ts b/packages/ai-config/src/model-capabilities/__tests__/infer.test.ts index 3dc2d5c..1a1b428 100644 --- a/packages/ai-config/src/model-capabilities/__tests__/infer.test.ts +++ b/packages/ai-config/src/model-capabilities/__tests__/infer.test.ts @@ -99,6 +99,11 @@ describe("inferModelCapabilities", () => { } }); + it("omits off but allows max for GPT-6.1 Sol", () => { + const levels = inferModelCapabilities("openai", "gpt-6.1-sol").facts.thinkingEffortLevels; + expect(levels).toEqual(["low", "medium", "high", "xhigh", "max"]); + }); + it("keeps Mantle GPT-5.6 output unknown while completing the operational view", () => { const caps = inferModelCapabilities("bedrock", "openai.gpt-5.6-sol"); expect(caps.facts.maxContextLength).toBe(1_000_000); diff --git a/packages/ai-config/src/model-capabilities/openai-helpers.ts b/packages/ai-config/src/model-capabilities/openai-helpers.ts index 890d844..2669301 100644 --- a/packages/ai-config/src/model-capabilities/openai-helpers.ts +++ b/packages/ai-config/src/model-capabilities/openai-helpers.ts @@ -5,11 +5,11 @@ import type { InferredModelCapabilities as ModelInfo } from "../types.js"; const OPENAI_THINKING_EFFORT_LEVELS = ["off", "low", "medium", "high"]; -// GPT-6 adds the "xhigh"/"max" levels. Astra has no "none" level, so its list -// omits "off": the product's "off" maps onto OpenAI's "none" (the OpenAI +// GPT-6 adds the "xhigh"/"max" levels. Astra and 6.1 Sol have no "none" level, +// so their list omits "off": the product's "off" maps onto OpenAI's "none" (the OpenAI // client omits reasoning_effort; Bedrock Mantle sends the wire value "none"). const GPT6_THINKING_EFFORT_LEVELS = ["off", "low", "medium", "high", "xhigh", "max"]; -const GPT6_ASTRA_THINKING_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"]; +const GPT6_NO_OFF_THINKING_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"]; /** * Determine OpenAI model capabilities based on ID. @@ -116,11 +116,12 @@ export function getOpenAIModelCapabilities(modelId: string): Partial }; } - // Sources verified 2026-09-22: + // Sources verified 2026-09-22 (Astra) and 2026-09-30 (6.1 Sol): // https://developers.openai.com/api/docs/models/gpt-6-astra - // OpenAI documents the same 1.05M window and 128K output maximum for Astra, - // Sol, and Luna. Astra has no "none" effort level, so its list omits "off". - if (/^gpt-6-astra(?:-|$)/.test(modelId)) { + // https://developers.openai.com/api/docs/models/gpt-6.1-sol + // OpenAI documents a 1.05M window and 128K output maximum for both; + // neither offers "none" effort, so the product must not offer "off". + if (/^gpt-6(?:-astra|\.1-sol)(?:-|$)/.test(modelId)) { return { family: "gpt-6", supportsTools: true, @@ -135,7 +136,7 @@ export function getOpenAIModelCapabilities(modelId: string): Partial supportsToolResultImages: true, maxContextLength: 1_050_000, maxOutputTokens: 128000, - thinkingEffortLevels: GPT6_ASTRA_THINKING_EFFORT_LEVELS, + thinkingEffortLevels: GPT6_NO_OFF_THINKING_EFFORT_LEVELS, }; } diff --git a/packages/ai-provider-bridge/src/model-clients/__tests__/openai-explicit-prompt-caching-wire.test.ts b/packages/ai-provider-bridge/src/model-clients/__tests__/openai-explicit-prompt-caching-wire.test.ts index a31f986..0a8feb7 100644 --- a/packages/ai-provider-bridge/src/model-clients/__tests__/openai-explicit-prompt-caching-wire.test.ts +++ b/packages/ai-provider-bridge/src/model-clients/__tests__/openai-explicit-prompt-caching-wire.test.ts @@ -2,11 +2,11 @@ * Copyright (C) 2026 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import type { ModelMessage } from "ai"; +import { jsonSchema, type ModelMessage } from "ai"; import { describe, expect, it } from "vitest"; import { createRawFetchCapture } from "../../../tests/helpers/raw-fetch-capture"; -import type { CancellationToken } from "../../types"; +import type { AiToolWithJsonSchema, CancellationToken } from "../../types"; import { OpenAIClient } from "../OpenAIClient"; const cancellationToken: CancellationToken = { @@ -147,6 +147,8 @@ async function captureRequest(options: { protocol?: "openai-chat" | "openai-responses"; metadata?: { sessionId?: string }; messages: ModelMessage[]; + tools?: Record; + thinkingEffort?: string; }): Promise> { const fetchCapture = createRawFetchCapture( async () => @@ -168,7 +170,8 @@ async function captureRequest(options: { messages: options.messages, metadata: options.metadata, usesExplicitPromptCaching: options.usesExplicitPromptCaching, - thinkingEffort: "high", + thinkingEffort: options.thinkingEffort ?? "high", + tools: options.tools, allowSystemInMessages: true, cancellationToken, }); @@ -184,6 +187,46 @@ async function captureRequest(options: { } describe("OpenAI explicit prompt caching wire requests", () => { + it("sends GPT-6.1 Sol via Responses with max reasoning, a function tool, and explicit caching", async () => { + const requestBody = await captureRequest({ + apiMode: "responses", + usesExplicitPromptCaching: true, + model: "gpt-6.1-sol", + metadata: { sessionId: "sol-conversation" }, + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: "Look up the answer", + providerOptions: breakpointProviderOptions, + }, + ], + }, + ], + thinkingEffort: "max", + tools: { + lookup: { + inputSchema: jsonSchema({ type: "object", properties: { query: { type: "string" } } }), + }, + }, + }); + + expect(requestBody).toMatchObject({ + model: "gpt-6.1-sol", + prompt_cache_key: "sol-conversation", + prompt_cache_options: { mode: "explicit", ttl: "30m" }, + store: false, + reasoning: { effort: "max", summary: "detailed" }, + tool_choice: "auto", + }); + expect(requestBody.tools).toEqual( + expect.arrayContaining([expect.objectContaining({ type: "function", name: "lookup" })]), + ); + expect(breakpointPaths(requestBody)).toEqual(["input[0].content[0].prompt_cache_breakpoint"]); + }); + it("serializes Responses options and a structured tool-result breakpoint", async () => { const messages = markedContinuationMessages(); const requestBody = await captureRequest({ diff --git a/packages/ai-provider-bridge/src/providers/__tests__/openai-provider.test.ts b/packages/ai-provider-bridge/src/providers/__tests__/openai-provider.test.ts index 08b9f8b..801d7fd 100644 --- a/packages/ai-provider-bridge/src/providers/__tests__/openai-provider.test.ts +++ b/packages/ai-provider-bridge/src/providers/__tests__/openai-provider.test.ts @@ -39,6 +39,7 @@ describe("OpenAI model discovery", () => { listingResponse([ "gpt-6-astra", "gpt-6-sol-2026-09-22", + "gpt-6.1-sol", "gpt-6-luna", "gpt-5.4", "text-embedding-3-large", @@ -53,6 +54,7 @@ describe("OpenAI model discovery", () => { expect(models.map((model) => model.id)).toEqual([ "gpt-6-astra", "gpt-6-sol-2026-09-22", + "gpt-6.1-sol", "gpt-6-luna", "gpt-5.4", ]); @@ -60,6 +62,10 @@ describe("OpenAI model discovery", () => { expect(astra?.name).toBe("GPT-6 Astra"); expect(astra?.maxContextLength).toBe(1_050_000); expect(astra?.maxOutputTokens).toBe(128_000); + const sol = models.find((model) => model.id === "gpt-6.1-sol"); + expect(sol?.name).toBe("GPT-6.1 Sol"); + expect(sol?.thinkingEffortLevels).toContain("max"); + expect(sol?.thinkingEffortLevels).not.toContain("off"); }); it("includes GPT-6 rows in the static fallback when the listing fails", async () => { @@ -75,7 +81,11 @@ describe("OpenAI model discovery", () => { const models = await registry.getModelsForProvider("openai", credentials); expect(models.map((model) => model.id)).toEqual( - expect.arrayContaining(["gpt-6-astra", "gpt-6-sol", "gpt-6-luna"]), + expect.arrayContaining(["gpt-6-astra", "gpt-6-sol", "gpt-6.1-sol", "gpt-6-luna"]), ); + expect(models.find((model) => model.id === "gpt-6.1-sol")).toMatchObject({ + name: "GPT-6.1 Sol", + thinkingEffortLevels: ["low", "medium", "high", "xhigh", "max"], + }); }); }); diff --git a/packages/ai-provider-bridge/src/providers/openai-model-names.ts b/packages/ai-provider-bridge/src/providers/openai-model-names.ts index fb30220..32ce56d 100644 --- a/packages/ai-provider-bridge/src/providers/openai-model-names.ts +++ b/packages/ai-provider-bridge/src/providers/openai-model-names.ts @@ -27,6 +27,7 @@ export const OPENAI_MODEL_NAMES: Record = { "gpt-5.6-luna": "GPT-5.6 Luna", "gpt-6-astra": "GPT-6 Astra", "gpt-6-sol": "GPT-6 Sol", + "gpt-6.1-sol": "GPT-6.1 Sol", "gpt-6-luna": "GPT-6 Luna", "gpt-5-mini": "GPT-5 Mini", "gpt-5-nano": "GPT-5 Nano", diff --git a/packages/ai-provider-bridge/src/providers/openai-provider.ts b/packages/ai-provider-bridge/src/providers/openai-provider.ts index 28f9948..07bc0e6 100644 --- a/packages/ai-provider-bridge/src/providers/openai-provider.ts +++ b/packages/ai-provider-bridge/src/providers/openai-provider.ts @@ -32,6 +32,7 @@ const OPENAI_DEFAULT_CAPABILITIES = { const OPENAI_FALLBACK_ROWS = [ { id: "gpt-6-astra", name: "GPT-6 Astra" }, { id: "gpt-6-sol", name: "GPT-6 Sol" }, + { id: "gpt-6.1-sol", name: "GPT-6.1 Sol" }, { id: "gpt-6-luna", name: "GPT-6 Luna" }, { id: "gpt-5.4", name: "GPT-5.4" }, { id: "gpt-5.4-mini", name: "GPT-5.4 Mini" },