diff --git a/packages/fold-agent/src/Catalog/BakedCatalog.ts b/packages/fold-agent/src/Catalog/BakedCatalog.ts index e08e270..35f9c49 100644 --- a/packages/fold-agent/src/Catalog/BakedCatalog.ts +++ b/packages/fold-agent/src/Catalog/BakedCatalog.ts @@ -814,6 +814,37 @@ export const bakedModelCatalog: ReadonlyArray = [ toolCall: true, pricing: { inputPerMTokens: 10, outputPerMTokens: 50, cacheReadPerMTokens: 1, cacheWritePerMTokens: 12.5 }, }, + { + providerId: 'openai', + modelId: 'gpt-6-luna', + name: 'GPT-6 Luna', + contextWindow: 1050000, + maxInputTokens: 922000, + maxOutputTokens: 128000, + reasoning: true, + reasoningEfforts: ['none', 'low', 'medium', 'high', 'xhigh', 'max'], + vision: true, + toolCall: true, + pricing: { + inputPerMTokens: 0.1, + outputPerMTokens: 0.5, + cacheReadPerMTokens: 0.01, + cacheWritePerMTokens: 0.125, + }, + }, + { + providerId: 'openai', + modelId: 'gpt-6-sol', + name: 'GPT-6 Sol', + contextWindow: 1050000, + maxInputTokens: 922000, + maxOutputTokens: 128000, + reasoning: true, + reasoningEfforts: ['none', 'low', 'medium', 'high', 'xhigh', 'max'], + vision: true, + toolCall: true, + pricing: { inputPerMTokens: 2, outputPerMTokens: 10, cacheReadPerMTokens: 0.2, cacheWritePerMTokens: 2.5 }, + }, { providerId: 'openai', modelId: 'gpt-image-1', diff --git a/packages/fold-agent/test/Catalog/BakedCatalog.vi.test.ts b/packages/fold-agent/test/Catalog/BakedCatalog.vi.test.ts index 0bc736f..4435257 100644 --- a/packages/fold-agent/test/Catalog/BakedCatalog.vi.test.ts +++ b/packages/fold-agent/test/Catalog/BakedCatalog.vi.test.ts @@ -46,6 +46,15 @@ const xaiGrok: ActiveModel = { reasoning: { _tag: 'effort', effort: 'xhigh' }, } +const codexModel = (modelId: string): ActiveModel => ({ + providerId: 'codex', + providerKind: 'codex', + modelId, + role: null, + requestedReasoningLevel: 'max', + reasoning: { _tag: 'effort', effort: 'max', summary: 'auto' }, +}) + it('resolves a codex-kind gpt-5.6-sol to the baked openai entry', () => { const entry = lookupCatalogEntry(bakedModelCatalog, codexSol) @@ -71,6 +80,30 @@ it('resolves a codex-kind gpt-6-astra to the baked OpenAI entry', () => { expect(entry?.reasoningEfforts).toContain('max') }) +it('ships the GPT-6 Sol and Luna public limits, efforts, pricing, and Codex provider lookup', () => { + const expectations = [ + { modelId: 'gpt-6-sol', inputPrice: 2, outputPrice: 10 }, + { modelId: 'gpt-6-luna', inputPrice: 0.1, outputPrice: 0.5 }, + ] as const + + for (const expected of expectations) { + const entry = lookupCatalogEntry(bakedModelCatalog, codexModel(expected.modelId)) + + expect(entry).not.toBeNull() + expect(entry).toMatchObject({ + providerId: 'openai', + modelId: expected.modelId, + contextWindow: 1_050_000, + maxInputTokens: 922_000, + maxOutputTokens: 128_000, + reasoning: true, + reasoningEfforts: ['none', 'low', 'medium', 'high', 'xhigh', 'max'], + }) + expect(entry?.pricing?.inputPerMTokens).toBe(expected.inputPrice) + expect(entry?.pricing?.outputPerMTokens).toBe(expected.outputPrice) + } +}) + it('resolves an openai-compatible gpt-5.6-terra to the baked openai entry', () => { const entry = lookupCatalogEntry(bakedModelCatalog, openAiTerra) diff --git a/packages/fold-agent/test/Config/AgentModels.vi.test.ts b/packages/fold-agent/test/Config/AgentModels.vi.test.ts index 0bc6d95..eaddd33 100644 --- a/packages/fold-agent/test/Config/AgentModels.vi.test.ts +++ b/packages/fold-agent/test/Config/AgentModels.vi.test.ts @@ -266,6 +266,24 @@ it.effect("an openai-compat binding for gpt-5.6-luna with reasoning 'max' resolv }), ) +it.effect("GPT-6 Sol and Luna bindings with reasoning 'max' resolve against the baked catalog", () => + Effect.gen(function* () { + const config = yield* parseFoldConfig(`{ + "providers": { "codex": { "kind": "codex" } }, + "roles": { + "smart": { "provider": "codex", "model": "gpt-6-sol", "reasoning": "max" }, + "fast": { "provider": "codex", "model": "gpt-6-luna", "reasoning": "max" } + } + }`) + const models = agentModelsFromConfig(config, { env: env({}), catalog: bakedModelCatalog }) + + const smart = yield* models.resolve('smart') + const fast = yield* models.resolve('fast') + expect(smart.activeModel).toMatchObject({ modelId: 'gpt-6-sol', requestedReasoningLevel: 'max' }) + expect(fast.activeModel).toMatchObject({ modelId: 'gpt-6-luna', requestedReasoningLevel: 'max' }) + }), +) + it.effect("gpt-5.5 with reasoning 'max' fails RoleResolutionError naming its supported levels", () => Effect.gen(function* () { const config = yield* parseFoldConfig(gpt56ConfigText('gpt-5.5')) diff --git a/packages/fold-codex/test/CodexModel.vi.test.ts b/packages/fold-codex/test/CodexModel.vi.test.ts index a74036d..dd30bc3 100644 --- a/packages/fold-codex/test/CodexModel.vi.test.ts +++ b/packages/fold-codex/test/CodexModel.vi.test.ts @@ -278,4 +278,17 @@ describe('codexModel defaults', () => { it('an explicit model wins over the default', () => { expect(codexModel({ model: 'gpt-5.5' }).activeModel.modelId).toBe('gpt-5.5') }) + + it('preserves the canonical GPT-6 Sol and Luna ids and max reasoning', () => { + for (const modelId of ['gpt-6-sol', 'gpt-6-luna']) { + const model = codexModel({ model: modelId, reasoning: 'max' }) + + expect(model.activeModel.modelId).toBe(modelId) + expect(model.activeModel).toMatchObject({ + providerKind: 'codex', + requestedReasoningLevel: 'max', + reasoning: { _tag: 'effort', effort: 'max', summary: 'auto' }, + }) + } + }) }) diff --git a/packages/fold-core/src/Compaction/CompactionEngine.ts b/packages/fold-core/src/Compaction/CompactionEngine.ts index f84407d..ba01b57 100644 --- a/packages/fold-core/src/Compaction/CompactionEngine.ts +++ b/packages/fold-core/src/Compaction/CompactionEngine.ts @@ -37,7 +37,7 @@ const filePartEstimateChars = 4_800 */ export const defaultModelContextWindows: ReadonlyArray = [ [/claude|fable|mythos/, 200_000], - [/gpt-6-astra/, 272_000], + [/gpt-6-(?:astra|sol|luna)/, 272_000], [/gpt-5|codex/, 272_000], [/gpt-4\.1/, 1_000_000], ] diff --git a/packages/fold-core/test/Api/ResumeSession.vi.test.ts b/packages/fold-core/test/Api/ResumeSession.vi.test.ts index b2234ea..bb684b5 100644 --- a/packages/fold-core/test/Api/ResumeSession.vi.test.ts +++ b/packages/fold-core/test/Api/ResumeSession.vi.test.ts @@ -19,6 +19,7 @@ import { resumeSession, startSession, ToolCallId, + type ActiveModel, type EventLogService, } from '../../src/index' import { textTurn } from '../TestLayers/ScriptedLanguageModel' @@ -165,6 +166,45 @@ it.effect('resume with a different model binding writes one epoch transition (D1 }).pipe(Effect.scoped, Effect.provide(NodeFileSystem.layer)), ) +it.effect('resume records exactly one durable transition from GPT-6 Sol to Luna', () => + Effect.gen(function* () { + const sharedLog = yield* makeSharedLog + const sol: ActiveModel = { + ...gptActiveModel, + providerId: 'codex', + providerKind: 'codex', + modelId: 'gpt-6-sol', + requestedReasoningLevel: 'max', + reasoning: { _tag: 'effort', effort: 'max', summary: 'auto' }, + } + const luna: ActiveModel = { ...sol, modelId: 'gpt-6-luna' } + + yield* Effect.scoped( + Effect.gen(function* () { + const first = yield* scriptedModel(sol, [textTurn('first answer')]) + const session = yield* startSession({ + agent: defineAgent({ model: first.model, systemPrompt: 'You are the assistant.' }), + log: eventLogSource(Effect.succeed(sharedLog)), + }) + yield* session.send('go') + }), + ) + + const resumed = yield* scriptedModel(luna, [textTurn('second answer')]) + const session = yield* resumeSession({ + agent: defineAgent({ model: resumed.model, systemPrompt: 'You are the assistant.' }), + log: eventLogSource(Effect.succeed(sharedLog)), + }) + const entries = yield* session.entries + const modelChanges = entries.filter((entry) => Predicate.isTagged(entry, 'model-change')) + + expect(modelChanges).toHaveLength(1) + expect(modelChanges[0]).toMatchObject({ model: { modelId: 'gpt-6-luna' } }) + yield* session.send('continue') + expect((yield* resumed.scripted.requests)[0]?.openAiConfig?.model).toBe('gpt-6-luna') + }).pipe(Effect.scoped, Effect.provide(NodeFileSystem.layer)), +) + it.effect('resume with changed leading blocks transitions too (D20 resume rule)', () => Effect.gen(function* () { const sharedLog = yield* makeSharedLog diff --git a/packages/fold-core/test/Api/SwitchModel.vi.test.ts b/packages/fold-core/test/Api/SwitchModel.vi.test.ts index b73fcd4..3e900be 100644 --- a/packages/fold-core/test/Api/SwitchModel.vi.test.ts +++ b/packages/fold-core/test/Api/SwitchModel.vi.test.ts @@ -251,6 +251,36 @@ it.effect('switchModel records thinking-change when the reasoning level changes }).pipe(Effect.scoped, Effect.provide(NodeFileSystem.layer)), ) +it.effect('switchModel preserves exact GPT-6 Sol and Luna ids in the durable transition and requests', () => + Effect.gen(function* () { + const sol: ActiveModel = { + ...gptActiveModel, + providerId: 'codex', + providerKind: 'codex', + modelId: 'gpt-6-sol', + requestedReasoningLevel: 'max', + reasoning: { _tag: 'effort', effort: 'max', summary: 'auto' }, + } + const luna: ActiveModel = { ...sol, modelId: 'gpt-6-luna' } + const first = yield* scriptedModel(sol, [textTurn('from sol')]) + const second = yield* scriptedModel(luna, [textTurn('from luna')]) + const session = yield* startSession({ agent: defineAgent({ model: first.model }) }) + + yield* session.send('first turn') + yield* session.switchModel(second.model, { reason: 'switch canonical model' }) + yield* session.send('second turn') + + const entries = yield* session.entries + const modelChanges = entries.filter((entry): entry is ModelChangeLogEntry => + Predicate.isTagged(entry, 'model-change'), + ) + expect(modelChanges).toHaveLength(1) + expect(modelChanges[0]?.model.modelId).toBe('gpt-6-luna') + expect((yield* first.scripted.requests)[0]?.openAiConfig?.model).toBe('gpt-6-sol') + expect((yield* second.scripted.requests)[0]?.openAiConfig?.model).toBe('gpt-6-luna') + }).pipe(Effect.scoped, Effect.provide(NodeFileSystem.layer)), +) + it.effect('switchModel rejects duplicate tool names in the replacement toolset as a defect', () => Effect.gen(function* () { const first = yield* scriptedModel(gptActiveModel, [textTurn('one')]) diff --git a/packages/fold-core/test/Compaction/AutoCompactSession.vi.test.ts b/packages/fold-core/test/Compaction/AutoCompactSession.vi.test.ts index ac3575c..bdd7c45 100644 --- a/packages/fold-core/test/Compaction/AutoCompactSession.vi.test.ts +++ b/packages/fold-core/test/Compaction/AutoCompactSession.vi.test.ts @@ -605,6 +605,37 @@ it.effect('a Codex model uses the app context window instead of the public API c }).pipe(Effect.scoped, Effect.provide(NodeFileSystem.layer)), ) +it.effect('GPT-6 Sol and Luna compact against the 272k Codex window instead of the public catalog', () => + Effect.gen(function* () { + for (const modelId of ['gpt-6-sol', 'gpt-6-luna']) { + const activeModel = { ...codexActiveModel, modelId } + const { model, scripted } = yield* scriptedModel(activeModel, [ + textTurn(`noted ${'a'.repeat(120)}`, { inputTokens: 224_000 }), + textTurn(`## Goal\n- ${modelId} summary`), + textTurn(`${modelId} answer`), + ]) + + const session = yield* startSession({ + agent: defineAgent({ model, autoCompact: { enabled: true, keepRecentTokens: 10 } }), + catalog: [ + { + ...scriptedCatalogEntry(1_050_000), + providerId: 'openai', + modelId, + }, + ], + }) + + yield* session.send(`${modelId} topic one`) + const finished = yield* session.send(`${modelId} topic two`) + + expect(finished.resultText).toBe(`${modelId} answer`) + expect(compactionEntries(yield* session.entries)).toHaveLength(1) + expect(yield* scripted.remainingTurns).toBe(0) + } + }).pipe(Effect.scoped, Effect.provide(NodeFileSystem.layer)), +) + it.effect('an explicit autoCompact.contextWindow beats the catalog entry', () => Effect.gen(function* () { const { model, scripted } = yield* scriptedModel(gptActiveModel, [ diff --git a/packages/fold-core/test/Compaction/CompactionEngine.vi.test.ts b/packages/fold-core/test/Compaction/CompactionEngine.vi.test.ts index adc9ae9..7d95e32 100644 --- a/packages/fold-core/test/Compaction/CompactionEngine.vi.test.ts +++ b/packages/fold-core/test/Compaction/CompactionEngine.vi.test.ts @@ -148,6 +148,8 @@ it('defaultContextWindowFor uses the interim table with a conservative fallback' expect(defaultContextWindowFor('gpt-5.5')).toBe(272_000) expect(defaultContextWindowFor('gpt-5.3-codex')).toBe(272_000) expect(defaultContextWindowFor('gpt-6-astra')).toBe(272_000) + expect(defaultContextWindowFor('gpt-6-sol')).toBe(272_000) + expect(defaultContextWindowFor('gpt-6-luna')).toBe(272_000) expect(defaultContextWindowFor('some-unknown-model')).toBe(128_000) expect(defaultContextWindowFor(null)).toBe(128_000) }) @@ -157,6 +159,8 @@ it('compactionUsableTokens applies the D11 formula, clamped for tiny windows', ( expect(compactionUsableTokens({ contextWindow: 200_000, reserveTokens: 16_384 })).toBe(151_616) // Tiny windows clamp the output budget to window/4 and the reserve to window/8, staying positive. expect(compactionUsableTokens({ contextWindow: 2_000, reserveTokens: 16_384 })).toBe(1_250) + // Fold's raw 272k Codex window compacts at 223,616 for Sol and Luna. + expect(compactionUsableTokens({ contextWindow: 272_000, reserveTokens: 16_384 })).toBe(223_616) }) // ── Stale-usage / progress guard ───────────────────────────────────────────── diff --git a/packages/fold-core/test/Model/ModelFamily.vi.test.ts b/packages/fold-core/test/Model/ModelFamily.vi.test.ts index 1be2a5f..0e11de3 100644 --- a/packages/fold-core/test/Model/ModelFamily.vi.test.ts +++ b/packages/fold-core/test/Model/ModelFamily.vi.test.ts @@ -38,6 +38,8 @@ it('classifies openai-compatible models by model id patterns', () => { expect(modelFamilyFor(openAiCompatible('gpt-5.5'))).toBe('gpt') expect(modelFamilyFor(openAiCompatible('o3-mini'))).toBe('gpt') expect(modelFamilyFor(openAiCompatible('gpt-5.1-codex-max'))).toBe('codex') + expect(modelFamilyFor(openAiCompatible('gpt-6-sol'))).toBe('gpt') + expect(modelFamilyFor(openAiCompatible('gpt-6-luna'))).toBe('gpt') expect(modelFamilyFor(openAiCompatible('anthropic/claude-opus-4-8'))).toBe('claude') expect(modelFamilyFor(openAiCompatible('llama-3.3-70b'))).toBe('unknown') })