Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
17 changes: 9 additions & 8 deletions packages/ai-config/src/model-capabilities/openai-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -116,11 +116,12 @@ export function getOpenAIModelCapabilities(modelId: string): Partial<ModelInfo>
};
}

// 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,
Expand All @@ -135,7 +136,7 @@ export function getOpenAIModelCapabilities(modelId: string): Partial<ModelInfo>
supportsToolResultImages: true,
maxContextLength: 1_050_000,
maxOutputTokens: 128000,
thinkingEffortLevels: GPT6_ASTRA_THINKING_EFFORT_LEVELS,
thinkingEffortLevels: GPT6_NO_OFF_THINKING_EFFORT_LEVELS,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -147,6 +147,8 @@ async function captureRequest(options: {
protocol?: "openai-chat" | "openai-responses";
metadata?: { sessionId?: string };
messages: ModelMessage[];
tools?: Record<string, AiToolWithJsonSchema>;
thinkingEffort?: string;
}): Promise<Record<string, unknown>> {
const fetchCapture = createRawFetchCapture(
async () =>
Expand All @@ -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,
});
Expand All @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -53,13 +54,18 @@ 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",
]);
const astra = models.find((model) => model.id === "gpt-6-astra");
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 () => {
Expand All @@ -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"],
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const OPENAI_MODEL_NAMES: Record<string, string> = {
"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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
Loading