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
31 changes: 31 additions & 0 deletions packages/fold-agent/src/Catalog/BakedCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,37 @@ export const bakedModelCatalog: ReadonlyArray<ModelCatalogEntry> = [
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',
Expand Down
33 changes: 33 additions & 0 deletions packages/fold-agent/test/Catalog/BakedCatalog.vi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)

Expand Down
18 changes: 18 additions & 0 deletions packages/fold-agent/test/Config/AgentModels.vi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down
13 changes: 13 additions & 0 deletions packages/fold-codex/test/CodexModel.vi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
})
}
})
})
2 changes: 1 addition & 1 deletion packages/fold-core/src/Compaction/CompactionEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const filePartEstimateChars = 4_800
*/
export const defaultModelContextWindows: ReadonlyArray<readonly [RegExp, number]> = [
[/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],
]
Expand Down
40 changes: 40 additions & 0 deletions packages/fold-core/test/Api/ResumeSession.vi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
resumeSession,
startSession,
ToolCallId,
type ActiveModel,
type EventLogService,
} from '../../src/index'
import { textTurn } from '../TestLayers/ScriptedLanguageModel'
Expand Down Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions packages/fold-core/test/Api/SwitchModel.vi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')])
Expand Down
31 changes: 31 additions & 0 deletions packages/fold-core/test/Compaction/AutoCompactSession.vi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand All @@ -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 ─────────────────────────────────────────────
Expand Down
2 changes: 2 additions & 0 deletions packages/fold-core/test/Model/ModelFamily.vi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
Loading