diff --git a/components/generation/generation-toolbar.tsx b/components/generation/generation-toolbar.tsx index 242c0f67a4..a1bf36a083 100644 --- a/components/generation/generation-toolbar.tsx +++ b/components/generation/generation-toolbar.tsx @@ -37,6 +37,7 @@ import { import type { SettingsSection } from '@/lib/types/settings'; import { MediaPopover } from '@/components/generation/media-popover'; import { getAcceptStringForProviders, isMimeSupportedByProviders } from '@/lib/document/mime'; +import { findModelById, modelIdsMatch } from '@/lib/ai/model-aliases'; // ─── Constants ─────────────────────────────────────────────── const MAX_COURSE_MATERIAL_SIZE_MB = 50; @@ -103,13 +104,21 @@ export function GenerationToolbar({ isServerConfigured: config.isServerConfigured, models: config.isServerConfigured && !config.apiKey && config.serverModels?.length - ? config.models.filter((m) => new Set(config.serverModels).has(m.id)) + ? config.models.filter((model) => + config.serverModels?.some((serverModelId) => + modelIdsMatch(id, model.id, serverModelId), + ), + ) : config.models, })) : []; const currentProviderConfig = providersConfig?.[currentProviderId]; - const currentModel = currentProviderConfig?.models.find((model) => model.id === currentModelId); + const currentModel = findModelById( + currentProviderId, + currentProviderConfig?.models, + currentModelId, + ); const currentThinkingConfig = thinkingConfigs[getThinkingConfigKey(currentProviderId, currentModelId)]; diff --git a/components/scene-renderers/pbl/v2/submission.tsx b/components/scene-renderers/pbl/v2/submission.tsx index ee46c99db0..c9312653ac 100644 --- a/components/scene-renderers/pbl/v2/submission.tsx +++ b/components/scene-renderers/pbl/v2/submission.tsx @@ -42,6 +42,7 @@ import { } from 'lucide-react'; import { addSubmission, listSubmissionsForMicrotask } from '@/lib/pbl/v2/operations/submission'; +import { findModelById } from '@/lib/ai/model-aliases'; import { TEXT_PDF_IMAGE_ACCEPT, isImageFile, @@ -920,7 +921,7 @@ function SubmissionModal({ // Whether the currently-selected model can read images. Reactive so that // switching models (in Settings) updates the image-caption gating live. const hasVision = useSettingsStore((s) => { - const model = s.providersConfig[s.providerId]?.models.find((m) => m.id === s.modelId); + const model = findModelById(s.providerId, s.providersConfig[s.providerId]?.models, s.modelId); return !!model?.capabilities?.vision; }); const [mode, setMode] = useState<'paste' | 'file'>('paste'); diff --git a/lib/ai/llm.ts b/lib/ai/llm.ts index 2daa3a245f..04f8cfb19b 100644 --- a/lib/ai/llm.ts +++ b/lib/ai/llm.ts @@ -10,6 +10,7 @@ import { createLogger } from '@/lib/logger'; import { PROVIDERS } from './providers'; import { thinkingContext } from './thinking-context'; import { getModelMetadataKey } from './model-metadata'; +import { getCanonicalModelId } from './model-aliases'; import type { ThinkingCapability, ThinkingConfig } from '@/lib/types/provider'; import { getThinkingMode, @@ -140,9 +141,10 @@ function buildThinkingProviderOptions( modelId: string, config: ThinkingConfig, ): ProviderOptions | undefined { + const lookupModelId = providerId ? getCanonicalModelId(providerId, modelId) : modelId; const info = providerId - ? MODEL_THINKING_MAP.get(getModelMetadataKey(providerId, modelId)) - : UNIQUE_MODEL_THINKING_MAP.get(modelId); + ? MODEL_THINKING_MAP.get(getModelMetadataKey(providerId, lookupModelId)) + : UNIQUE_MODEL_THINKING_MAP.get(lookupModelId); if (!info?.thinking) return undefined; // model has no thinking capability const thinking = info.thinking; if (thinking.control === 'none') return undefined; @@ -278,8 +280,9 @@ const DEFAULT_VALIDATE = (text: string) => text.trim().length > 0; // --------------------------------------------------------------------------- function buildUsageMeta(params: GenerateTextParams | StreamTextParams, source: string) { - const modelId = getModelId(params); + const rawModelId = getModelId(params); const providerId = getModelProviderId(params) ?? 'unknown'; + const modelId = getCanonicalModelId(providerId, rawModelId); return { source, providerId, modelId, modelString: `${providerId}:${modelId}` }; } diff --git a/lib/ai/model-aliases.ts b/lib/ai/model-aliases.ts new file mode 100644 index 0000000000..2e4122e4a5 --- /dev/null +++ b/lib/ai/model-aliases.ts @@ -0,0 +1,23 @@ +const MODEL_ID_ALIASES: ReadonlyMap = new Map([['openai:gpt-5.6-sol', 'gpt-5.6']]); + +/** Resolve aliases used for local catalog, settings, capability, and usage lookups. */ +export function getCanonicalModelId(providerId: string, modelId: string): string { + return MODEL_ID_ALIASES.get(`${providerId}:${modelId}`) ?? modelId; +} + +export function modelIdsMatch(providerId: string, left: string, right: string): boolean { + return getCanonicalModelId(providerId, left) === getCanonicalModelId(providerId, right); +} + +/** Find a model using canonical IDs without changing the model ID sent on the wire. */ +export function findModelById( + providerId: string, + models: readonly T[] | undefined, + modelId: string, +): T | undefined { + const canonicalModelId = getCanonicalModelId(providerId, modelId); + return ( + models?.find((model) => model.id === canonicalModelId) ?? + models?.find((model) => modelIdsMatch(providerId, model.id, modelId)) + ); +} diff --git a/lib/ai/model-metadata.ts b/lib/ai/model-metadata.ts index a4efb4a80e..b1de668de3 100644 --- a/lib/ai/model-metadata.ts +++ b/lib/ai/model-metadata.ts @@ -6,6 +6,7 @@ import type { ThinkingLevel, ThinkingRequestAdapter, } from '@/lib/types/provider'; +import { getCanonicalModelId } from './model-aliases'; export function getModelMetadataKey(providerId: string, modelId: string): string { return `${providerId}:${modelId}`; @@ -227,7 +228,21 @@ const doubaoSeed20Effort: ThinkingCapability = { const minimaxM3Thinking = toggleCapability('anthropic', false); +const openaiGpt56Effort: ThinkingCapability = { + control: 'effort', + requestAdapter: 'openai', + effortValues: ['none', 'low', 'medium', 'high', 'xhigh', 'max'], + defaultEffort: 'medium', + defaultMode: 'enabled', + toggleable: true, + budgetAdjustable: true, + defaultEnabled: true, +}; + const THINKING_CAPABILITIES: Record = { + [getModelMetadataKey('openai', 'gpt-5.6')]: openaiGpt56Effort, + [getModelMetadataKey('openai', 'gpt-5.6-terra')]: openaiGpt56Effort, + [getModelMetadataKey('openai', 'gpt-5.6-luna')]: openaiGpt56Effort, [getModelMetadataKey('openai', 'gpt-5.5')]: effortCapability( 'openai', ['low', 'medium', 'high', 'xhigh'], @@ -403,7 +418,8 @@ export function getCatalogThinkingCapability( providerId: string, modelId: string, ): ThinkingCapability | undefined { - const exact = THINKING_CAPABILITIES[getModelMetadataKey(providerId, modelId)]; + const canonicalModelId = getCanonicalModelId(providerId, modelId); + const exact = THINKING_CAPABILITIES[getModelMetadataKey(providerId, canonicalModelId)]; if (exact) return exact; if (providerId === 'lemonade') { diff --git a/lib/ai/providers.ts b/lib/ai/providers.ts index 09d46e991e..000dd18301 100644 --- a/lib/ai/providers.ts +++ b/lib/ai/providers.ts @@ -39,6 +39,7 @@ import type { ThinkingConfig, } from '@/lib/types/provider'; import { applyModelMetadata, getCatalogThinkingCapability } from './model-metadata'; +import { findModelById } from './model-aliases'; import { getDefaultThinkingConfig, getThinkingMode, pickThinkingBudget } from './thinking-config'; import { createLogger } from '@/lib/logger'; // NOTE: Do NOT import thinking-context.ts here — it uses node:async_hooks @@ -66,6 +67,54 @@ export const PROVIDERS: Record = { requiresApiKey: true, icon: '/logos/openai.svg', models: [ + { + id: 'gpt-5.6', + name: 'GPT-5.6 Sol', + contextWindow: 1050000, + outputWindow: 128000, + capabilities: { + streaming: true, + tools: true, + vision: true, + thinking: { + toggleable: true, + budgetAdjustable: true, + defaultEnabled: true, + }, + }, + }, + { + id: 'gpt-5.6-terra', + name: 'GPT-5.6 Terra', + contextWindow: 1050000, + outputWindow: 128000, + capabilities: { + streaming: true, + tools: true, + vision: true, + thinking: { + toggleable: true, + budgetAdjustable: true, + defaultEnabled: true, + }, + }, + }, + { + id: 'gpt-5.6-luna', + name: 'GPT-5.6 Luna', + contextWindow: 1050000, + outputWindow: 128000, + capabilities: { + streaming: true, + tools: true, + vision: true, + thinking: { + toggleable: true, + budgetAdjustable: true, + defaultEnabled: true, + }, + }, + }, { id: 'gpt-5.5', name: 'GPT-5.5', @@ -1421,6 +1470,7 @@ function shouldUseOpenAIResponsesApi(providerId: ProviderId, modelId: string): b return ( /^gpt-5\.\d+-pro(?:-|$)/.test(modelId) || + /^gpt-5\.6(?:-|$)/.test(modelId) || /^gpt-5\.5(?:-|$)/.test(modelId) || /^gpt-5\.[3-9]-codex(?:-|$)/.test(modelId) ); @@ -1659,7 +1709,7 @@ export function getModel(config: ModelConfig): ModelWithInfo { } // Look up model info from the provider registry - const modelInfo = provider?.models.find((m) => m.id === config.modelId) || null; + const modelInfo = findModelById(config.providerId, provider?.models, config.modelId) ?? null; return { model, modelInfo }; } @@ -1713,5 +1763,5 @@ export function getProvider(providerId: ProviderId): ProviderConfig | undefined */ export function getModelInfo(providerId: ProviderId, modelId: string): ModelInfo | undefined { const provider = PROVIDERS[providerId]; - return provider?.models.find((m) => m.id === modelId); + return findModelById(providerId, provider?.models, modelId); } diff --git a/lib/ai/thinking-config.ts b/lib/ai/thinking-config.ts index 4409e805ea..03ef04722b 100644 --- a/lib/ai/thinking-config.ts +++ b/lib/ai/thinking-config.ts @@ -5,9 +5,10 @@ import type { ThinkingLevel, ThinkingMode, } from '@/lib/types/provider'; +import { getCanonicalModelId } from './model-aliases'; export function getThinkingConfigKey(providerId: string, modelId: string): string { - return `${providerId}:${modelId}`; + return `${providerId}:${getCanonicalModelId(providerId, modelId)}`; } export function supportsConfigurableThinking( diff --git a/lib/config/apply-token-plan.ts b/lib/config/apply-token-plan.ts index 74c66dface..78448f09de 100644 --- a/lib/config/apply-token-plan.ts +++ b/lib/config/apply-token-plan.ts @@ -10,6 +10,7 @@ import type { import { MODALITY_ORDER } from './token-plan-presets'; import { getCatalogThinkingCapability } from '@/lib/ai/model-metadata'; import { PROVIDERS } from '@/lib/ai/providers'; +import { findModelById } from '@/lib/ai/model-aliases'; import type { ModelInfo } from '@/lib/types/provider'; /** @@ -104,7 +105,11 @@ export function applyTokenPlan( } function catalogModelFor(target: TokenPlanModalityTarget, id: string): ModelInfo | undefined { - const direct = PROVIDERS[target.providerId as ProviderId]?.models.find((m) => m.id === id); + const direct = findModelById( + target.providerId, + PROVIDERS[target.providerId as ProviderId]?.models, + id, + ); if (direct) return direct; const allModels = Object.values(PROVIDERS).flatMap((provider) => provider.models); diff --git a/lib/store/settings.ts b/lib/store/settings.ts index 29ddb67ca3..eb41640a04 100644 --- a/lib/store/settings.ts +++ b/lib/store/settings.ts @@ -8,6 +8,7 @@ import { persist } from 'zustand/middleware'; import type { ProviderId } from '@/lib/ai/providers'; import type { ProvidersConfig } from '@/lib/types/settings'; import { PROVIDERS } from '@/lib/ai/providers'; +import { findModelById, getCanonicalModelId } from '@/lib/ai/model-aliases'; import type { ThinkingConfig } from '@/lib/types/provider'; import { getThinkingConfigKey, supportsConfigurableThinking } from '@/lib/ai/thinking-config'; import type { TTSProviderId, ASRProviderId, BuiltInTTSProviderId } from '@/lib/audio/types'; @@ -393,11 +394,28 @@ function resolveLLMSelection( ? currentProviderId : ((Object.keys(config) as ProviderId[]).find(isUsable) ?? ('' as ProviderId)); const modelId = providerId - ? resolveSelectedModel(currentModelId, config[providerId]?.models ?? []) + ? resolveSelectedLLMModel(providerId, currentModelId, config[providerId]?.models ?? []) : ''; return { providerId, modelId }; } +/** Keep the caller's wire model ID when it resolves to a known catalog alias. */ +function resolveSelectedLLMModel( + providerId: ProviderId, + currentModelId: string, + availableModels: Array<{ id: string }>, +): string { + if (availableModels.some((model) => model.id === currentModelId)) return currentModelId; + const canonicalModelId = getCanonicalModelId(providerId, currentModelId); + if ( + canonicalModelId !== currentModelId && + availableModels.some((model) => model.id === canonicalModelId) + ) { + return currentModelId; + } + return availableModels[0]?.id ?? ''; +} + function resolveMediaModels( builtInModels: T[], config?: { customModels?: T[]; replaceBuiltInModels?: boolean }, @@ -1376,9 +1394,27 @@ export const useSettingsStore = create()( const currentModels = newProvidersConfig[key].models; // When server specifies allowed models, filter the models list // while preserving custom IDs from env/YAML in server order. - const currentModelMap = new Map(currentModels.map((m) => [m.id, m])); const filteredModels = info.models?.length - ? info.models.map((id) => currentModelMap.get(id) ?? { id, name: id }) + ? info.models.map((id) => { + const currentModel = findModelById(key, currentModels, id); + const builtInModel = findModelById(key, PROVIDERS[key]?.models, id); + const model = + currentModel && builtInModel + ? { + ...builtInModel, + ...currentModel, + name: + currentModel.name === currentModel.id + ? builtInModel.name + : currentModel.name, + capabilities: { + ...builtInModel.capabilities, + ...currentModel.capabilities, + }, + } + : (currentModel ?? builtInModel); + return model ? { ...model, id, name: model.name || id } : { id, name: id }; + }) : currentModels; newProvidersConfig[key] = { ...newProvidersConfig[key], @@ -1606,7 +1642,7 @@ export const useSettingsStore = create()( ? (newProvidersConfig[validLLMProvider as ProviderId]?.models ?? []) : []; const validLLMModel = validLLMProvider - ? resolveSelectedModel(state.modelId, llmModels) + ? resolveSelectedLLMModel(validLLMProvider as ProviderId, state.modelId, llmModels) : ''; const imageModels = validImageProvider ? resolveMediaModels( diff --git a/lib/utils/model-config.ts b/lib/utils/model-config.ts index 4c4f2d4777..425b8bc4f4 100644 --- a/lib/utils/model-config.ts +++ b/lib/utils/model-config.ts @@ -4,6 +4,8 @@ import { normalizeThinkingConfig, supportsConfigurableThinking, } from '@/lib/ai/thinking-config'; +import { findModelById } from '@/lib/ai/model-aliases'; +import { getCatalogThinkingCapability } from '@/lib/ai/model-metadata'; /** * Get current model configuration from settings store @@ -14,8 +16,9 @@ export function getCurrentModelConfig() { // Get current provider's config const providerConfig = providersConfig[providerId]; - const modelInfo = providerConfig?.models.find((model) => model.id === modelId); - const thinking = modelInfo?.capabilities?.thinking; + const modelInfo = findModelById(providerId, providerConfig?.models, modelId); + const thinking = + modelInfo?.capabilities?.thinking ?? getCatalogThinkingCapability(providerId, modelId); const thinkingConfig = supportsConfigurableThinking(thinking) ? normalizeThinkingConfig(thinking, thinkingConfigs[getThinkingConfigKey(providerId, modelId)]) : undefined; diff --git a/package.json b/package.json index 54bf398ac0..cfdd8ea879 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "dependencies": { "@ai-sdk/anthropic": "^3.0.71", "@ai-sdk/google": "^3.0.64", - "@ai-sdk/openai": "^3.0.53", + "@ai-sdk/openai": "^3.0.84", "@ai-sdk/react": "^3.0.170", "@assistant-ui/react": "^0.14.18", "@assistant-ui/react-markdown": "^0.14.1", diff --git a/packages/docs/content/docs/supported-models.ar.mdx b/packages/docs/content/docs/supported-models.ar.mdx index 59dfeff35e..07164f53be 100644 --- a/packages/docs/content/docs/supported-models.ar.mdx +++ b/packages/docs/content/docs/supported-models.ar.mdx @@ -11,7 +11,7 @@ description: قوائم النماذج والموفرين المدمجة في إ | الموفر | Provider ID | النماذج المدمجة | | --------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| OpenAI | `openai` | GPT-5.5 (`gpt-5.5`)
GPT-5.4 Pro (`gpt-5.4-pro`)
GPT-5.4 (`gpt-5.4`)
GPT-5.4 Mini (`gpt-5.4-mini`)
GPT-5.4 Nano (`gpt-5.4-nano`) | +| OpenAI | `openai` | GPT-5.6 Sol (`gpt-5.6`)
GPT-5.6 Terra (`gpt-5.6-terra`)
GPT-5.6 Luna (`gpt-5.6-luna`)
GPT-5.5 (`gpt-5.5`)
GPT-5.4 Pro (`gpt-5.4-pro`)
GPT-5.4 (`gpt-5.4`)
GPT-5.4 Mini (`gpt-5.4-mini`)
GPT-5.4 Nano (`gpt-5.4-nano`) | | Claude | `anthropic` | Claude Opus 4.7 (`claude-opus-4-7`)
Claude Opus 4.6 (`claude-opus-4-6`)
Claude Sonnet 4.6 (`claude-sonnet-4-6`)
Claude Sonnet 4.5 (`claude-sonnet-4-5`)
Claude Haiku 4.5 (`claude-haiku-4-5`) | | Gemini | `google` | Gemini 3.1 Pro Preview (`gemini-3.1-pro-preview`)
Gemini 3 Flash Preview (`gemini-3-flash-preview`)
Gemini 2.5 Flash (`gemini-2.5-flash`)
Gemini 2.5 Flash Lite (`gemini-2.5-flash-lite`)
Gemini 2.5 Pro (`gemini-2.5-pro`) | | GLM | `glm` | GLM-5.1 (`glm-5.1`)
GLM-5V-Turbo (`glm-5v-turbo`)
GLM-5 (`glm-5`)
GLM-4.7 (`glm-4.7`)
GLM-4.7-FlashX (`glm-4.7-flashx`)
GLM-4.7-Flash (`glm-4.7-flash`)
GLM-4.6 (`glm-4.6`)
GLM-4.6V (`glm-4.6v`)
GLM-4.6V-Flash (`glm-4.6v-flash`) | @@ -84,4 +84,3 @@ description: قوائم النماذج والموفرين المدمجة في إ | unpdf | `unpdf` | نصوص، صور، metadata | | MinerU | `mineru` | نصوص، صور، جداول، صيغ رياضية، layout analysis | | MinerU Cloud | `mineru-cloud` | نصوص، صور، جداول، صيغ رياضية، layout analysis | - diff --git a/packages/docs/content/docs/supported-models.ja.mdx b/packages/docs/content/docs/supported-models.ja.mdx index ede0211c10..a77dbcb062 100644 --- a/packages/docs/content/docs/supported-models.ja.mdx +++ b/packages/docs/content/docs/supported-models.ja.mdx @@ -11,7 +11,7 @@ description: オープンソース版 OpenMAIC の組み込みモデルと provi | プロバイダー | Provider ID | 組み込みモデル | | --------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| OpenAI | `openai` | GPT-5.5 (`gpt-5.5`)
GPT-5.4 Pro (`gpt-5.4-pro`)
GPT-5.4 (`gpt-5.4`)
GPT-5.4 Mini (`gpt-5.4-mini`)
GPT-5.4 Nano (`gpt-5.4-nano`) | +| OpenAI | `openai` | GPT-5.6 Sol (`gpt-5.6`)
GPT-5.6 Terra (`gpt-5.6-terra`)
GPT-5.6 Luna (`gpt-5.6-luna`)
GPT-5.5 (`gpt-5.5`)
GPT-5.4 Pro (`gpt-5.4-pro`)
GPT-5.4 (`gpt-5.4`)
GPT-5.4 Mini (`gpt-5.4-mini`)
GPT-5.4 Nano (`gpt-5.4-nano`) | | Claude | `anthropic` | Claude Opus 4.7 (`claude-opus-4-7`)
Claude Opus 4.6 (`claude-opus-4-6`)
Claude Sonnet 4.6 (`claude-sonnet-4-6`)
Claude Sonnet 4.5 (`claude-sonnet-4-5`)
Claude Haiku 4.5 (`claude-haiku-4-5`) | | Gemini | `google` | Gemini 3.1 Pro Preview (`gemini-3.1-pro-preview`)
Gemini 3 Flash Preview (`gemini-3-flash-preview`)
Gemini 2.5 Flash (`gemini-2.5-flash`)
Gemini 2.5 Flash Lite (`gemini-2.5-flash-lite`)
Gemini 2.5 Pro (`gemini-2.5-pro`) | | GLM | `glm` | GLM-5.1 (`glm-5.1`)
GLM-5V-Turbo (`glm-5v-turbo`)
GLM-5 (`glm-5`)
GLM-4.7 (`glm-4.7`)
GLM-4.7-FlashX (`glm-4.7-flashx`)
GLM-4.7-Flash (`glm-4.7-flash`)
GLM-4.6 (`glm-4.6`)
GLM-4.6V (`glm-4.6v`)
GLM-4.6V-Flash (`glm-4.6v-flash`) | @@ -84,4 +84,3 @@ description: オープンソース版 OpenMAIC の組み込みモデルと provi | unpdf | `unpdf` | テキスト、画像、metadata | | MinerU | `mineru` | テキスト、画像、表、数式、レイアウト解析 | | MinerU Cloud | `mineru-cloud` | テキスト、画像、表、数式、レイアウト解析 | - diff --git a/packages/docs/content/docs/supported-models.mdx b/packages/docs/content/docs/supported-models.mdx index eb17bf6f8c..6b349ef2dd 100644 --- a/packages/docs/content/docs/supported-models.mdx +++ b/packages/docs/content/docs/supported-models.mdx @@ -11,7 +11,7 @@ Use model IDs as `provider:model-id` in `DEFAULT_MODEL` or YAML. For example: `o | Provider | Provider ID | Built-in models | | --------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| OpenAI | `openai` | GPT-5.5 (`gpt-5.5`)
GPT-5.4 Pro (`gpt-5.4-pro`)
GPT-5.4 (`gpt-5.4`)
GPT-5.4 Mini (`gpt-5.4-mini`)
GPT-5.4 Nano (`gpt-5.4-nano`) | +| OpenAI | `openai` | GPT-5.6 Sol (`gpt-5.6`)
GPT-5.6 Terra (`gpt-5.6-terra`)
GPT-5.6 Luna (`gpt-5.6-luna`)
GPT-5.5 (`gpt-5.5`)
GPT-5.4 Pro (`gpt-5.4-pro`)
GPT-5.4 (`gpt-5.4`)
GPT-5.4 Mini (`gpt-5.4-mini`)
GPT-5.4 Nano (`gpt-5.4-nano`) | | Claude | `anthropic` | Claude Opus 4.7 (`claude-opus-4-7`)
Claude Opus 4.6 (`claude-opus-4-6`)
Claude Sonnet 4.6 (`claude-sonnet-4-6`)
Claude Sonnet 4.5 (`claude-sonnet-4-5`)
Claude Haiku 4.5 (`claude-haiku-4-5`) | | Gemini | `google` | Gemini 3.1 Pro Preview (`gemini-3.1-pro-preview`)
Gemini 3 Flash Preview (`gemini-3-flash-preview`)
Gemini 2.5 Flash (`gemini-2.5-flash`)
Gemini 2.5 Flash Lite (`gemini-2.5-flash-lite`)
Gemini 2.5 Pro (`gemini-2.5-pro`) | | GLM | `glm` | GLM-5.1 (`glm-5.1`)
GLM-5V-Turbo (`glm-5v-turbo`)
GLM-5 (`glm-5`)
GLM-4.7 (`glm-4.7`)
GLM-4.7-FlashX (`glm-4.7-flashx`)
GLM-4.7-Flash (`glm-4.7-flash`)
GLM-4.6 (`glm-4.6`)
GLM-4.6V (`glm-4.6v`)
GLM-4.6V-Flash (`glm-4.6v-flash`) | @@ -84,4 +84,3 @@ This table lists model selectors and self-hosted backends. It does not enumerate | unpdf | `unpdf` | Text, images, metadata | | MinerU | `mineru` | Text, images, tables, formulas, layout analysis | | MinerU Cloud | `mineru-cloud` | Text, images, tables, formulas, layout analysis | - diff --git a/packages/docs/content/docs/supported-models.ru.mdx b/packages/docs/content/docs/supported-models.ru.mdx index a4787476f8..d26c859a1b 100644 --- a/packages/docs/content/docs/supported-models.ru.mdx +++ b/packages/docs/content/docs/supported-models.ru.mdx @@ -11,7 +11,7 @@ description: Встроенные списки моделей и providers дл | Provider | Provider ID | Встроенные модели | | --------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| OpenAI | `openai` | GPT-5.5 (`gpt-5.5`)
GPT-5.4 Pro (`gpt-5.4-pro`)
GPT-5.4 (`gpt-5.4`)
GPT-5.4 Mini (`gpt-5.4-mini`)
GPT-5.4 Nano (`gpt-5.4-nano`) | +| OpenAI | `openai` | GPT-5.6 Sol (`gpt-5.6`)
GPT-5.6 Terra (`gpt-5.6-terra`)
GPT-5.6 Luna (`gpt-5.6-luna`)
GPT-5.5 (`gpt-5.5`)
GPT-5.4 Pro (`gpt-5.4-pro`)
GPT-5.4 (`gpt-5.4`)
GPT-5.4 Mini (`gpt-5.4-mini`)
GPT-5.4 Nano (`gpt-5.4-nano`) | | Claude | `anthropic` | Claude Opus 4.7 (`claude-opus-4-7`)
Claude Opus 4.6 (`claude-opus-4-6`)
Claude Sonnet 4.6 (`claude-sonnet-4-6`)
Claude Sonnet 4.5 (`claude-sonnet-4-5`)
Claude Haiku 4.5 (`claude-haiku-4-5`) | | Gemini | `google` | Gemini 3.1 Pro Preview (`gemini-3.1-pro-preview`)
Gemini 3 Flash Preview (`gemini-3-flash-preview`)
Gemini 2.5 Flash (`gemini-2.5-flash`)
Gemini 2.5 Flash Lite (`gemini-2.5-flash-lite`)
Gemini 2.5 Pro (`gemini-2.5-pro`) | | GLM | `glm` | GLM-5.1 (`glm-5.1`)
GLM-5V-Turbo (`glm-5v-turbo`)
GLM-5 (`glm-5`)
GLM-4.7 (`glm-4.7`)
GLM-4.7-FlashX (`glm-4.7-flashx`)
GLM-4.7-Flash (`glm-4.7-flash`)
GLM-4.6 (`glm-4.6`)
GLM-4.6V (`glm-4.6v`)
GLM-4.6V-Flash (`glm-4.6v-flash`) | @@ -84,4 +84,3 @@ description: Встроенные списки моделей и providers дл | unpdf | `unpdf` | Текст, изображения, metadata | | MinerU | `mineru` | Текст, изображения, таблицы, формулы, layout analysis | | MinerU Cloud | `mineru-cloud` | Текст, изображения, таблицы, формулы, layout analysis | - diff --git a/packages/docs/content/docs/supported-models.zh-cn.mdx b/packages/docs/content/docs/supported-models.zh-cn.mdx index a075455ad3..f8ccce8415 100644 --- a/packages/docs/content/docs/supported-models.zh-cn.mdx +++ b/packages/docs/content/docs/supported-models.zh-cn.mdx @@ -11,7 +11,7 @@ description: 开源版 OpenMAIC 的内置模型与 provider 列表。 | 提供方 | Provider ID | 内置模型 | | --------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| OpenAI | `openai` | GPT-5.5 (`gpt-5.5`)
GPT-5.4 Pro (`gpt-5.4-pro`)
GPT-5.4 (`gpt-5.4`)
GPT-5.4 Mini (`gpt-5.4-mini`)
GPT-5.4 Nano (`gpt-5.4-nano`) | +| OpenAI | `openai` | GPT-5.6 Sol (`gpt-5.6`)
GPT-5.6 Terra (`gpt-5.6-terra`)
GPT-5.6 Luna (`gpt-5.6-luna`)
GPT-5.5 (`gpt-5.5`)
GPT-5.4 Pro (`gpt-5.4-pro`)
GPT-5.4 (`gpt-5.4`)
GPT-5.4 Mini (`gpt-5.4-mini`)
GPT-5.4 Nano (`gpt-5.4-nano`) | | Claude | `anthropic` | Claude Opus 4.7 (`claude-opus-4-7`)
Claude Opus 4.6 (`claude-opus-4-6`)
Claude Sonnet 4.6 (`claude-sonnet-4-6`)
Claude Sonnet 4.5 (`claude-sonnet-4-5`)
Claude Haiku 4.5 (`claude-haiku-4-5`) | | Gemini | `google` | Gemini 3.1 Pro Preview (`gemini-3.1-pro-preview`)
Gemini 3 Flash Preview (`gemini-3-flash-preview`)
Gemini 2.5 Flash (`gemini-2.5-flash`)
Gemini 2.5 Flash Lite (`gemini-2.5-flash-lite`)
Gemini 2.5 Pro (`gemini-2.5-pro`) | | GLM | `glm` | GLM-5.1 (`glm-5.1`)
GLM-5V-Turbo (`glm-5v-turbo`)
GLM-5 (`glm-5`)
GLM-4.7 (`glm-4.7`)
GLM-4.7-FlashX (`glm-4.7-flashx`)
GLM-4.7-Flash (`glm-4.7-flash`)
GLM-4.6 (`glm-4.6`)
GLM-4.6V (`glm-4.6v`)
GLM-4.6V-Flash (`glm-4.6v-flash`) | @@ -84,4 +84,3 @@ description: 开源版 OpenMAIC 的内置模型与 provider 列表。 | unpdf | `unpdf` | 文本、图片、元数据 | | MinerU | `mineru` | 文本、图片、表格、公式、版面分析 | | MinerU Cloud | `mineru-cloud` | 文本、图片、表格、公式、版面分析 | - diff --git a/packages/docs/content/docs/supported-models.zh-tw.mdx b/packages/docs/content/docs/supported-models.zh-tw.mdx index 4323f250dc..68eeca78a2 100644 --- a/packages/docs/content/docs/supported-models.zh-tw.mdx +++ b/packages/docs/content/docs/supported-models.zh-tw.mdx @@ -11,7 +11,7 @@ description: 開源版 OpenMAIC 的內建模型與 provider 清單。 | 供應商 | Provider ID | 內建模型 | | --------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| OpenAI | `openai` | GPT-5.5 (`gpt-5.5`)
GPT-5.4 Pro (`gpt-5.4-pro`)
GPT-5.4 (`gpt-5.4`)
GPT-5.4 Mini (`gpt-5.4-mini`)
GPT-5.4 Nano (`gpt-5.4-nano`) | +| OpenAI | `openai` | GPT-5.6 Sol (`gpt-5.6`)
GPT-5.6 Terra (`gpt-5.6-terra`)
GPT-5.6 Luna (`gpt-5.6-luna`)
GPT-5.5 (`gpt-5.5`)
GPT-5.4 Pro (`gpt-5.4-pro`)
GPT-5.4 (`gpt-5.4`)
GPT-5.4 Mini (`gpt-5.4-mini`)
GPT-5.4 Nano (`gpt-5.4-nano`) | | Claude | `anthropic` | Claude Opus 4.7 (`claude-opus-4-7`)
Claude Opus 4.6 (`claude-opus-4-6`)
Claude Sonnet 4.6 (`claude-sonnet-4-6`)
Claude Sonnet 4.5 (`claude-sonnet-4-5`)
Claude Haiku 4.5 (`claude-haiku-4-5`) | | Gemini | `google` | Gemini 3.1 Pro Preview (`gemini-3.1-pro-preview`)
Gemini 3 Flash Preview (`gemini-3-flash-preview`)
Gemini 2.5 Flash (`gemini-2.5-flash`)
Gemini 2.5 Flash Lite (`gemini-2.5-flash-lite`)
Gemini 2.5 Pro (`gemini-2.5-pro`) | | GLM | `glm` | GLM-5.1 (`glm-5.1`)
GLM-5V-Turbo (`glm-5v-turbo`)
GLM-5 (`glm-5`)
GLM-4.7 (`glm-4.7`)
GLM-4.7-FlashX (`glm-4.7-flashx`)
GLM-4.7-Flash (`glm-4.7-flash`)
GLM-4.6 (`glm-4.6`)
GLM-4.6V (`glm-4.6v`)
GLM-4.6V-Flash (`glm-4.6v-flash`) | @@ -84,4 +84,3 @@ description: 開源版 OpenMAIC 的內建模型與 provider 清單。 | unpdf | `unpdf` | 文字、圖片、metadata | | MinerU | `mineru` | 文字、圖片、表格、公式、版面分析 | | MinerU Cloud | `mineru-cloud` | 文字、圖片、表格、公式、版面分析 | - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2127739491..08bbb0cd7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^3.0.64 version: 3.0.64(zod@4.3.6) '@ai-sdk/openai': - specifier: ^3.0.53 - version: 3.0.53(zod@4.3.6) + specifier: ^3.0.84 + version: 3.0.84(zod@4.3.6) '@ai-sdk/react': specifier: ^3.0.170 version: 3.0.170(react@19.2.3)(zod@4.3.6) @@ -769,8 +769,8 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/openai@3.0.53': - resolution: {integrity: sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ==} + '@ai-sdk/openai@3.0.84': + resolution: {integrity: sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -787,6 +787,16 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@4.0.38': + resolution: {integrity: sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@3.0.14': + resolution: {integrity: sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA==} + engines: {node: '>=18'} + '@ai-sdk/provider@3.0.8': resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} engines: {node: '>=18'} @@ -12924,16 +12934,16 @@ snapshots: pkce-challenge: 5.0.1 zod: 3.25.76 - '@ai-sdk/openai@3.0.53(zod@3.25.76)': + '@ai-sdk/openai@3.0.84(zod@3.25.76)': dependencies: - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.23(zod@3.25.76) + '@ai-sdk/provider': 3.0.14 + '@ai-sdk/provider-utils': 4.0.38(zod@3.25.76) zod: 3.25.76 - '@ai-sdk/openai@3.0.53(zod@4.3.6)': + '@ai-sdk/openai@3.0.84(zod@4.3.6)': dependencies: - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.23(zod@4.3.6) + '@ai-sdk/provider': 3.0.14 + '@ai-sdk/provider-utils': 4.0.38(zod@4.3.6) zod: 4.3.6 '@ai-sdk/provider-utils@4.0.19(zod@3.25.76)': @@ -12957,6 +12967,24 @@ snapshots: eventsource-parser: 3.0.8 zod: 4.3.6 + '@ai-sdk/provider-utils@4.0.38(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.14 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.8 + zod: 3.25.76 + + '@ai-sdk/provider-utils@4.0.38(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.14 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.8 + zod: 4.3.6 + + '@ai-sdk/provider@3.0.14': + dependencies: + json-schema: 0.4.0 + '@ai-sdk/provider@3.0.8': dependencies: json-schema: 0.4.0 @@ -14464,7 +14492,7 @@ snapshots: '@ag-ui/core': 0.0.47 '@ag-ui/langgraph': 0.0.24(@ag-ui/client@0.0.47)(@ag-ui/core@0.0.47)(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.19.0)(zod@4.3.6))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@ai-sdk/anthropic': 3.0.71(zod@3.25.76) - '@ai-sdk/openai': 3.0.53(zod@3.25.76) + '@ai-sdk/openai': 3.0.84(zod@3.25.76) '@copilotkit/shared': 1.53.0(@ag-ui/core@0.0.47) '@copilotkitnext/agent': 1.53.0(@cfworker/json-schema@4.1.1) '@copilotkitnext/runtime': 1.53.0(@ag-ui/client@0.0.47)(@ag-ui/core@0.0.47)(@ag-ui/encoder@0.0.47)(@cfworker/json-schema@4.1.1)(@copilotkitnext/shared@1.53.0)(zod@3.25.76) @@ -14523,7 +14551,7 @@ snapshots: '@ai-sdk/anthropic': 3.0.71(zod@3.25.76) '@ai-sdk/google': 3.0.64(zod@3.25.76) '@ai-sdk/mcp': 1.0.25(zod@3.25.76) - '@ai-sdk/openai': 3.0.53(zod@3.25.76) + '@ai-sdk/openai': 3.0.84(zod@3.25.76) '@modelcontextprotocol/sdk': 1.27.1(@cfworker/json-schema@4.1.1)(zod@3.25.76) ai: 6.0.168(zod@3.25.76) rxjs: 7.8.1 diff --git a/tests/ai/llm-thinking-options.test.ts b/tests/ai/llm-thinking-options.test.ts index 65cae5e15c..36dc796ab3 100644 --- a/tests/ai/llm-thinking-options.test.ts +++ b/tests/ai/llm-thinking-options.test.ts @@ -1,18 +1,121 @@ -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; const aiMock = vi.hoisted(() => ({ - generateText: vi.fn(async (params: unknown) => ({ text: 'ok', params })), + generateText: vi.fn(async (params: unknown) => ({ + text: 'ok', + params, + usage: undefined as unknown, + })), streamText: vi.fn(), })); +const usageMock = vi.hoisted(() => ({ + normalizeUsage: vi.fn((usage: unknown) => usage), + recordUsage: vi.fn(async () => undefined), +})); + vi.mock('ai', () => ({ generateText: aiMock.generateText, streamText: aiMock.streamText, })); +vi.mock('@/lib/usage/normalize', () => ({ + normalizeUsage: usageMock.normalizeUsage, +})); + +vi.mock('@/lib/server/usage-storage', () => ({ + recordUsage: usageMock.recordUsage, +})); + import { callLLM } from '@/lib/ai/llm'; describe('LLM thinking provider options', () => { + beforeEach(() => { + aiMock.generateText.mockClear(); + usageMock.normalizeUsage.mockClear(); + usageMock.recordUsage.mockClear(); + }); + + it('sends GPT-5.6 max reasoning effort through OpenAI provider options', async () => { + await callLLM( + { + model: { + provider: 'openai.responses', + modelId: 'gpt-5.6', + }, + prompt: 'hi', + } as Parameters[0], + 'test', + undefined, + { mode: 'enabled', effort: 'max' }, + ); + + expect(aiMock.generateText).toHaveBeenCalledWith( + expect.objectContaining({ + providerOptions: { + openai: { + reasoningEffort: 'max', + }, + }, + }), + ); + }); + + it('sends max reasoning effort for the GPT-5.6 Sol model ID alias', async () => { + await callLLM( + { + model: { + provider: 'openai.responses', + modelId: 'gpt-5.6-sol', + }, + prompt: 'hi', + } as Parameters[0], + 'test', + undefined, + { mode: 'enabled', effort: 'max' }, + ); + + expect(aiMock.generateText).toHaveBeenCalledWith( + expect.objectContaining({ + model: expect.objectContaining({ modelId: 'gpt-5.6-sol' }), + providerOptions: { + openai: { + reasoningEffort: 'max', + }, + }, + }), + ); + }); + + it('aggregates GPT-5.6 Sol alias usage under the canonical model ID', async () => { + aiMock.generateText.mockResolvedValueOnce({ + text: 'ok', + params: undefined, + usage: { inputTokens: 1, outputTokens: 1 }, + }); + + await callLLM( + { + model: { + provider: 'openai.responses', + modelId: 'gpt-5.6-sol', + }, + prompt: 'hi', + } as Parameters[0], + 'test', + ); + + await vi.waitFor(() => { + expect(usageMock.recordUsage).toHaveBeenCalledWith( + expect.objectContaining({ + providerId: 'openai', + modelId: 'gpt-5.6', + modelString: 'openai:gpt-5.6', + }), + ); + }); + }); + it('sends Claude Haiku 4.5 thinking budget without effort', async () => { await callLLM( { diff --git a/tests/ai/model-aliases.test.ts b/tests/ai/model-aliases.test.ts new file mode 100644 index 0000000000..02646826fd --- /dev/null +++ b/tests/ai/model-aliases.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; + +import { findModelById, getCanonicalModelId, modelIdsMatch } from '@/lib/ai/model-aliases'; + +describe('model aliases', () => { + it('canonicalizes GPT-5.6 Sol only for OpenAI', () => { + expect(getCanonicalModelId('openai', 'gpt-5.6-sol')).toBe('gpt-5.6'); + expect(getCanonicalModelId('openrouter', 'gpt-5.6-sol')).toBe('gpt-5.6-sol'); + }); + + it('leaves canonical and unrelated model IDs unchanged', () => { + expect(getCanonicalModelId('openai', 'gpt-5.6')).toBe('gpt-5.6'); + expect(getCanonicalModelId('openai', 'gpt-5.6-terra')).toBe('gpt-5.6-terra'); + }); + + it('matches and finds an alias through its canonical model ID', () => { + const models = [ + { id: 'gpt-5.6-sol', vision: false }, + { id: 'gpt-5.6', vision: true }, + ]; + + expect(modelIdsMatch('openai', 'gpt-5.6', 'gpt-5.6-sol')).toBe(true); + expect(findModelById('openai', models, 'gpt-5.6-sol')).toBe(models[1]); + expect(findModelById('openrouter', [models[1]], 'gpt-5.6-sol')).toBeUndefined(); + }); +}); diff --git a/tests/ai/model-metadata.test.ts b/tests/ai/model-metadata.test.ts index 29de95b854..ee1caf2400 100644 --- a/tests/ai/model-metadata.test.ts +++ b/tests/ai/model-metadata.test.ts @@ -60,4 +60,10 @@ describe('model metadata thinking capabilities', () => { expect(getCatalogThinkingCapability('siliconflow', 'deepseek-ai/DeepSeek-V3.2')).toBeDefined(); expect(getCatalogThinkingCapability('lemonade', 'Gemma-4-26B-A4B-it-GGUF')).toBeDefined(); }); + + it('resolves GPT-5.6 Sol alias metadata through the canonical model ID', () => { + expect(getCatalogThinkingCapability('openai', 'gpt-5.6-sol')).toEqual( + getCatalogThinkingCapability('openai', 'gpt-5.6'), + ); + }); }); diff --git a/tests/ai/openai-provider.test.ts b/tests/ai/openai-provider.test.ts index b41b5b8296..e1010c6870 100644 --- a/tests/ai/openai-provider.test.ts +++ b/tests/ai/openai-provider.test.ts @@ -74,6 +74,53 @@ describe('OpenAI provider defaults', () => { }); }); + it.each([ + ['gpt-5.6', 'GPT-5.6 Sol'], + ['gpt-5.6-terra', 'GPT-5.6 Terra'], + ['gpt-5.6-luna', 'GPT-5.6 Luna'], + ])('includes %s as a built-in OpenAI model', (modelId, name) => { + expect(getModelInfo('openai', modelId)).toMatchObject({ + id: modelId, + name, + contextWindow: 1050000, + outputWindow: 128000, + capabilities: { + streaming: true, + tools: true, + vision: true, + thinking: { + control: 'effort', + requestAdapter: 'openai', + effortValues: ['none', 'low', 'medium', 'high', 'xhigh', 'max'], + defaultEffort: 'medium', + toggleable: true, + budgetAdjustable: true, + defaultEnabled: true, + }, + }, + }); + }); + + it.each(['gpt-5.6', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna'])( + 'routes %s through the OpenAI Responses API', + (modelId) => { + const { model, modelInfo } = getModel({ + providerId: 'openai', + modelId, + apiKey: 'sk-test', + }); + + expect(openAiMock.responses).toHaveBeenCalledWith(modelId); + expect(openAiMock.chat).not.toHaveBeenCalled(); + expect(model).toEqual({ endpoint: 'responses', modelId }); + expect(modelInfo).toBe(getModelInfo('openai', modelId)); + }, + ); + + it('resolves GPT-5.6 Sol model info through the canonical built-in entry', () => { + expect(getModelInfo('openai', 'gpt-5.6-sol')).toBe(getModelInfo('openai', 'gpt-5.6')); + }); + it('includes GPT-5.5 as a built-in OpenAI model', () => { expect(getModelInfo('openai', 'gpt-5.5')).toMatchObject({ id: 'gpt-5.5', diff --git a/tests/ai/openai-sdk-integration.test.ts b/tests/ai/openai-sdk-integration.test.ts new file mode 100644 index 0000000000..bf1f0c53ce --- /dev/null +++ b/tests/ai/openai-sdk-integration.test.ts @@ -0,0 +1,52 @@ +import { createOpenAI } from '@ai-sdk/openai'; +import { generateText } from 'ai'; +import { describe, expect, it } from 'vitest'; + +describe('OpenAI SDK integration', () => { + it('accepts GPT-5.6 max reasoning effort and sends it to the Responses API', async () => { + let requestBody: Record | undefined; + const fetchMock = async (_input: RequestInfo | URL, init?: RequestInit) => { + requestBody = JSON.parse(String(init?.body)) as Record; + + return new Response( + JSON.stringify({ + id: 'resp_test', + object: 'response', + created_at: 1, + status: 'completed', + model: 'gpt-5.6', + output: [ + { + id: 'msg_test', + type: 'message', + status: 'completed', + role: 'assistant', + content: [{ type: 'output_text', text: 'ok', annotations: [] }], + }, + ], + usage: { + input_tokens: 1, + input_tokens_details: { cached_tokens: 0 }, + output_tokens: 1, + output_tokens_details: { reasoning_tokens: 0 }, + total_tokens: 2, + }, + }), + { status: 200, headers: { 'content-type': 'application/json' } }, + ); + }; + const openai = createOpenAI({ apiKey: 'sk-test', fetch: fetchMock }); + + const result = await generateText({ + model: openai.responses('gpt-5.6'), + prompt: 'hi', + providerOptions: { openai: { reasoningEffort: 'max' } }, + }); + + expect(result.text).toBe('ok'); + expect(requestBody).toMatchObject({ + model: 'gpt-5.6', + reasoning: { effort: 'max' }, + }); + }); +}); diff --git a/tests/ai/thinking-config.test.ts b/tests/ai/thinking-config.test.ts index 76c23b3554..81d5baf035 100644 --- a/tests/ai/thinking-config.test.ts +++ b/tests/ai/thinking-config.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import { getProvider } from '@/lib/ai/providers'; import { + getThinkingConfigKey, getDefaultThinkingConfig, getThinkingDisplayValue, normalizeThinkingConfig, @@ -94,6 +95,11 @@ describe('thinking config metadata', () => { }); describe('thinking config normalization', () => { + it('shares one settings key between GPT-5.6 Sol and its alias', () => { + expect(getThinkingConfigKey('openai', 'gpt-5.6-sol')).toBe('openai:gpt-5.6'); + expect(getThinkingConfigKey('openai', 'gpt-5.6')).toBe('openai:gpt-5.6'); + }); + it('normalizes OpenAI effort defaults and selected effort values', () => { const thinking = getThinking('openai', 'gpt-5.4'); @@ -107,6 +113,27 @@ describe('thinking config normalization', () => { }); }); + it.each(['gpt-5.6', 'gpt-5.6-terra', 'gpt-5.6-luna'])( + 'normalizes %s with medium default and max effort', + (modelId) => { + const thinking = getThinking('openai', modelId); + + expect(getDefaultThinkingConfig(thinking)).toEqual({ + mode: 'enabled', + effort: 'medium', + }); + expect(normalizeThinkingConfig(thinking, { mode: 'disabled' })).toEqual({ + mode: 'disabled', + effort: 'none', + }); + expect(normalizeThinkingConfig(thinking, { effort: 'max' })).toEqual({ + mode: 'enabled', + effort: 'max', + }); + expect(thinking?.effortValues).toEqual(['none', 'low', 'medium', 'high', 'xhigh', 'max']); + }, + ); + it('normalizes GPT-5.5 as non-toggleable effort levels', () => { const thinking = getThinking('openai', 'gpt-5.5'); diff --git a/tests/config/apply-token-plan.test.ts b/tests/config/apply-token-plan.test.ts index f07c5d279a..ce8c3b4c69 100644 --- a/tests/config/apply-token-plan.test.ts +++ b/tests/config/apply-token-plan.test.ts @@ -150,6 +150,39 @@ describe('applyTokenPlan', () => { expect(cfg.models[1].capabilities?.thinking).toBeDefined(); }); + it('preserves GPT-5.6 Sol catalog metadata when a plan uses the explicit model ID', () => { + const actions = makeActions(); + const preset: TokenPlanPreset = { + ...deepseek, + modalities: { + llm: { + providerId: 'openai', + baseUrl: 'https://api.openai.com/v1', + apiFormat: 'openai', + defaultModels: ['gpt-5.6-sol'], + }, + }, + }; + + applyTokenPlan(preset, 'sk-test', actions); + const config = (actions.setProviderConfig as ReturnType).mock.calls[0][1] as { + models: Array<{ + id: string; + contextWindow?: number; + outputWindow?: number; + capabilities?: { vision?: boolean; thinking?: unknown }; + }>; + }; + + expect(config.models[0]).toMatchObject({ + id: 'gpt-5.6-sol', + contextWindow: 1050000, + outputWindow: 128000, + capabilities: { vision: true }, + }); + expect(config.models[0].capabilities?.thinking).toBeDefined(); + }); + it('isolates a failing modality without aborting the rest', () => { const actions = makeActions(); (actions.setImageProviderConfig as ReturnType).mockImplementation(() => { diff --git a/tests/pbl/v2/submission-flow.test.ts b/tests/pbl/v2/submission-flow.test.ts index 0886c43c56..5ea1577eaa 100644 --- a/tests/pbl/v2/submission-flow.test.ts +++ b/tests/pbl/v2/submission-flow.test.ts @@ -6,6 +6,8 @@ import { isSubmitLockedDuringStream, taskEvaluationCanAdvance, } from '@/components/scene-renderers/pbl/v2/submission'; +import { findModelById } from '@/lib/ai/model-aliases'; +import { PROVIDERS } from '@/lib/ai/providers'; import { isToleratedReactionStreamError } from '@/components/scene-renderers/pbl/v2/use-instructor-stream'; import type { PBLEvaluation } from '@/lib/pbl/v2/types'; import type { PBLSSEEvent } from '@/lib/pbl/v2/api/sse'; @@ -65,6 +67,19 @@ describe('PBL v2 — post-submission flow', () => { ); }); + it('recognizes the explicit GPT-5.6 Sol ID as vision-capable', () => { + const model = findModelById('openai', PROVIDERS.openai.models, 'gpt-5.6-sol'); + + expect(model?.capabilities?.vision).toBe(true); + expect( + imageRequiresCaption({ + hasImage: true, + hasVision: !!model?.capabilities?.vision, + hasCaption: false, + }), + ).toBe(false); + }); + it('tolerates a soft EMPTY_LLM_OUTPUT from the post-eval reaction turn, not other errors (#593)', () => { const empty: PBLSSEEvent = { type: 'error', diff --git a/tests/store/settings-server-sync.test.ts b/tests/store/settings-server-sync.test.ts index 3102181b7e..e2972f9f28 100644 --- a/tests/store/settings-server-sync.test.ts +++ b/tests/store/settings-server-sync.test.ts @@ -368,6 +368,69 @@ describe('fetchServerProviders — provider availability sync', () => { expect(models[1].name).toBe('GPT-4o'); }); + it('enriches a managed GPT-5.6 Sol alias with canonical catalog metadata', async () => { + const store = await getStore(); + store.setState({ + providersConfig: { + ...store.getState().providersConfig, + openai: { + ...store.getState().providersConfig.openai, + models: [ + { + id: 'gpt-5.6', + name: 'GPT-5.6 Sol', + contextWindow: 1050000, + outputWindow: 128000, + capabilities: { + vision: true, + thinking: { + requestAdapter: 'openai', + defaultEffort: 'medium', + }, + }, + }, + ], + }, + }, + }); + mockServerResponse({ + providers: { + openai: { models: ['gpt-5.6-sol'] }, + }, + }); + + await store.getState().fetchServerProviders(); + + const model = store.getState().providersConfig.openai.models[0]; + expect(model).toMatchObject({ + id: 'gpt-5.6-sol', + name: 'GPT-5.6 Sol', + contextWindow: 1050000, + outputWindow: 128000, + capabilities: { + vision: true, + thinking: { + requestAdapter: 'openai', + defaultEffort: 'medium', + }, + }, + }); + }); + + it('switches a canonical selection to the alias when the managed allowlist only permits it', async () => { + const store = await getStore(); + store.setState({ providerId: 'openai', modelId: 'gpt-5.6' }); + mockServerResponse({ + providers: { + openai: { models: ['gpt-5.6-sol'] }, + }, + }); + + await store.getState().fetchServerProviders(); + + expect(store.getState().modelId).toBe('gpt-5.6-sol'); + }); + it('keeps all models when server provides no model restriction', async () => { const store = await getStore(); mockServerResponse({ @@ -1174,6 +1237,26 @@ describe('usable provider ⇒ concrete model invariant (#580)', () => { expect(store.getState().modelId).toBe('gpt-4o'); }); + it('preserves an alias wire ID when provider config contains its canonical model', async () => { + const store = await getStore(); + store.setState({ + providerId: 'openai', + modelId: 'gpt-5.6-sol', + providersConfig: { + ...store.getState().providersConfig, + openai: { + ...store.getState().providersConfig.openai, + apiKey: 'sk-client', + models: [{ id: 'gpt-5.6', name: 'GPT-5.6 Sol' }], + }, + }, + }); + + store.getState().setProviderConfig('openai', { baseUrl: 'https://api.openai.com/v1' }); + + expect(store.getState().modelId).toBe('gpt-5.6-sol'); + }); + it('configuring a non-active provider does not hijack the current selection', async () => { const store = await getStore();