diff --git a/packages/fold-agent/src/Catalog/BakedCatalog.ts b/packages/fold-agent/src/Catalog/BakedCatalog.ts index 9fb45d3..e08e270 100644 --- a/packages/fold-agent/src/Catalog/BakedCatalog.ts +++ b/packages/fold-agent/src/Catalog/BakedCatalog.ts @@ -801,6 +801,19 @@ export const bakedModelCatalog: ReadonlyArray = [ toolCall: true, pricing: { inputPerMTokens: 2.5, outputPerMTokens: 15, cacheReadPerMTokens: 0.25, cacheWritePerMTokens: 3.125 }, }, + { + providerId: 'openai', + modelId: 'gpt-6-astra', + name: 'GPT-6 Astra', + contextWindow: 1050000, + maxInputTokens: 922000, + maxOutputTokens: 128000, + reasoning: true, + reasoningEfforts: ['low', 'medium', 'high', 'xhigh', 'max'], + vision: true, + toolCall: true, + pricing: { inputPerMTokens: 10, outputPerMTokens: 50, cacheReadPerMTokens: 1, cacheWritePerMTokens: 12.5 }, + }, { providerId: 'openai', modelId: 'gpt-image-1', diff --git a/packages/fold-agent/src/Config/AgentModels.ts b/packages/fold-agent/src/Config/AgentModels.ts index 14e05b6..84eaf87 100644 --- a/packages/fold-agent/src/Config/AgentModels.ts +++ b/packages/fold-agent/src/Config/AgentModels.ts @@ -6,7 +6,7 @@ * which carries no key (it uses `~/.fold/auth.json`). * * `orchestrator` falls back to `smart` when unbound (D25). A binding without a model fills the provider - * kind's default (codex → gpt-5.6-sol, anthropic → claude-opus-4-8); openai-compat has no default and + * kind's default (codex → gpt-6-astra, anthropic → claude-opus-4-8); openai-compat has no default and * requires an explicit model. Provider cross-references were validated at decode (ConfigSchema), so * remaining resolution failures are a missing credential env var, a model-less openai-compat binding, * or - when a model catalog is provided - a reasoning level the bound model does not support (D23: diff --git a/packages/fold-agent/src/Config/ConfigSchema.ts b/packages/fold-agent/src/Config/ConfigSchema.ts index a53357a..e231496 100644 --- a/packages/fold-agent/src/Config/ConfigSchema.ts +++ b/packages/fold-agent/src/Config/ConfigSchema.ts @@ -54,7 +54,7 @@ export const RoleBinding = Schema.Struct({ model: Schema.optionalKey( Schema.String.annotate({ description: - 'Provider model id; defaults: codex/opencode → gpt-5.6-sol, xai → grok-4.6, anthropic → claude-opus-4-8; required for openai-compat', + 'Provider model id; defaults: codex → gpt-6-astra, opencode → gpt-5.6-sol, xai → grok-4.6, anthropic → claude-opus-4-8; required for openai-compat', }), ), /** Reasoning level for this role's requests. Defaults to `off` when omitted. */ diff --git a/packages/fold-agent/src/Config/ConfigSchemaJson.ts b/packages/fold-agent/src/Config/ConfigSchemaJson.ts index 5c8644c..c992273 100644 --- a/packages/fold-agent/src/Config/ConfigSchemaJson.ts +++ b/packages/fold-agent/src/Config/ConfigSchemaJson.ts @@ -67,13 +67,14 @@ export const starterConfigJsonc = (): string => // Model roles bound to a provider (+ optional model). "smart" and "fast" are required; // "orchestrator" is optional and falls back to "smart". Omitting "model" uses the provider - // defaults: codex/opencode → gpt-5.6-sol, xai → grok-4.6, anthropic → claude-opus-4-8. + // defaults: codex → gpt-6-astra, opencode → gpt-5.6-sol, xai → grok-4.6, + // anthropic → claude-opus-4-8. "roles": { // Default primary model for new sessions. - "smart": { "provider": "codex", "model": "gpt-5.6-sol", "reasoning": "medium" }, + "smart": { "provider": "codex", "model": "gpt-6-astra", "reasoning": "medium" }, // An explicit model always wins over the provider default. "fast": { "provider": "anthropic", "model": "claude-haiku-4-5-20251001" }, - // No model: uses the codex default, gpt-5.6-sol. + // No model: uses the codex default, gpt-6-astra. "orchestrator": { "provider": "codex", "reasoning": "high" } }, diff --git a/packages/fold-agent/src/Config/FoldInfo.ts b/packages/fold-agent/src/Config/FoldInfo.ts index 36356b3..a5d5c3d 100644 --- a/packages/fold-agent/src/Config/FoldInfo.ts +++ b/packages/fold-agent/src/Config/FoldInfo.ts @@ -73,14 +73,14 @@ is optional and falls back to \`smart\`. | \`orchestrator\` | the RLM mode's primary agent | Each binding is \`{ "provider": , "model"?: , "reasoning"?: }\`. Omitting \`model\` -uses the provider kind's default: codex/opencode → \`gpt-5.6-sol\`, xai → \`grok-4.6\`, and +uses the provider kind's default: codex → \`gpt-6-astra\`, opencode → \`gpt-5.6-sol\`, xai → \`grok-4.6\`, and anthropic → \`claude-opus-4-8\` (openai-compat requires an explicit model). \`reasoning\` is one of \`off · minimal · low · medium · high · xhigh · max\` and is validated against the model catalog - a level the model does not support fails the launch naming the supported levels. All roles referenced by a launch resolve eagerly, so every binding needs working credentials. -The starter config defaults \`smart\` to Codex \`gpt-5.6-sol\` with \`medium\` reasoning, so a new +The starter config defaults \`smart\` to Codex \`gpt-6-astra\` with \`medium\` reasoning, so a new default-mode session uses that model unless the user selects a profile or direct model override. ## Profiles - named presets (roles + optional mode) @@ -143,7 +143,7 @@ foldcode --prompt "..." # one-shot run; exit codes 0/130/1 foldcode --profile ultracodex # profile's roles + its pinned mode (rlm) foldcode --mode rlm --rpi # explicit mode + RPI roster foldcode --role fast --reasoning off # primary on the fast role, reasoning off -foldcode --provider codex # kind change drops the stale model -> gpt-5.6-sol default +foldcode --provider codex # kind change drops the stale model -> gpt-6-astra default foldcode --resume latest|sess_* # resume (config re-resolves; drift writes a durable transition) foldcode sessions # list this project's session logs foldcode config init|validate # starter config + schema | validate diff --git a/packages/fold-agent/src/Config/ModelSelections.ts b/packages/fold-agent/src/Config/ModelSelections.ts index d738d1c..e0173c0 100644 --- a/packages/fold-agent/src/Config/ModelSelections.ts +++ b/packages/fold-agent/src/Config/ModelSelections.ts @@ -88,7 +88,7 @@ export const describeModelConfiguration = ( .filter((entry) => catalogProviderIds.includes(entry.providerId)) .map((entry) => entry.modelId) const defaultModels = Match.value(provider.kind).pipe( - Match.when('codex', () => [DEFAULT_OPENCODE_MODEL_ID]), + Match.when('codex', () => [DEFAULT_CODEX_MODEL_ID]), Match.when('opencode', () => [DEFAULT_OPENCODE_MODEL_ID, GROK_BUILD_MODEL_ID]), Match.when('xai', () => XAI_FRONTIER_MODELS.map(({ modelId }) => modelId)), Match.orElse((): ReadonlyArray => []), diff --git a/packages/fold-agent/test/Catalog/BakedCatalog.vi.test.ts b/packages/fold-agent/test/Catalog/BakedCatalog.vi.test.ts index 8ba8b9a..0bc736f 100644 --- a/packages/fold-agent/test/Catalog/BakedCatalog.vi.test.ts +++ b/packages/fold-agent/test/Catalog/BakedCatalog.vi.test.ts @@ -1,5 +1,5 @@ /** - * Baked-catalog resolution tests for the gpt-5.6 model family (D23): the codex provider kind maps + * Baked-catalog resolution tests for current Codex models (D23): the codex provider kind maps * onto the models.dev `openai` provider id, so a codex-kind ActiveModel resolves the same catalog * entry an openai-compatible one does. Asserting limits, pricing, and the `max` effort level against * the shipped data proves the entries that make `reasoning: max` valid for these models are live. @@ -19,6 +19,15 @@ const codexSol: ActiveModel = { reasoning: { _tag: 'effort', effort: 'max', summary: 'auto' }, } +const codexAstra: ActiveModel = { + providerId: 'codex', + providerKind: 'codex', + modelId: 'gpt-6-astra', + role: null, + requestedReasoningLevel: 'max', + reasoning: { _tag: 'effort', effort: 'max', summary: 'auto' }, +} + const openAiTerra: ActiveModel = { providerId: 'openai', providerKind: 'openai-compatible', @@ -49,6 +58,19 @@ it('resolves a codex-kind gpt-5.6-sol to the baked openai entry', () => { expect(entry?.reasoningEfforts).toContain('max') }) +it('resolves a codex-kind gpt-6-astra to the baked OpenAI entry', () => { + const entry = lookupCatalogEntry(bakedModelCatalog, codexAstra) + + expect(entry).not.toBeNull() + expect(entry?.providerId).toBe('openai') + expect(entry?.modelId).toBe('gpt-6-astra') + expect(entry?.contextWindow).toBe(1_050_000) + expect(entry?.maxInputTokens).toBe(922_000) + expect(entry?.pricing?.inputPerMTokens).toBe(10) + expect(entry?.pricing?.outputPerMTokens).toBe(50) + expect(entry?.reasoningEfforts).toContain('max') +}) + 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 1aac0f0..0bc6d95 100644 --- a/packages/fold-agent/test/Config/AgentModels.vi.test.ts +++ b/packages/fold-agent/test/Config/AgentModels.vi.test.ts @@ -294,7 +294,7 @@ const defaultsConfigText = `{ } }` -it.effect('a codex binding without a model resolves to the gpt-5.6-sol default', () => +it.effect('a codex binding without a model resolves to the gpt-6-astra default', () => Effect.gen(function* () { const config = yield* parseFoldConfig(defaultsConfigText) const models = agentModelsFromConfig(config, { @@ -304,8 +304,8 @@ it.effect('a codex binding without a model resolves to the gpt-5.6-sol default', const model = yield* models.resolve('smart') expect(model.activeModel.providerKind).toBe('codex') - expect(model.activeModel.modelId).toBe('gpt-5.6-sol') - // Catalog validation ran against the defaulted id: sol supports 'max'. + expect(model.activeModel.modelId).toBe('gpt-6-astra') + // Catalog validation ran against the defaulted id: Astra supports 'max'. expect(model.activeModel.requestedReasoningLevel).toBe('max') }), ) @@ -336,7 +336,7 @@ it.effect('an openai-compat binding without a model fails with the required-mode expect(error._tag).toBe('RoleResolutionError') expect(error.role).toBe('orchestrator') expect(error.message).toContain('without a model') - expect(error.message).toContain('codex → gpt-5.6-sol') + expect(error.message).toContain('codex → gpt-6-astra') expect(error.message).toContain('anthropic → claude-opus-4-8') expect(error.message).toContain('required for openai-compat') }), diff --git a/packages/fold-agent/test/Config/ConfigSchemaJson.vi.test.ts b/packages/fold-agent/test/Config/ConfigSchemaJson.vi.test.ts index 221ff1f..eccefec 100644 --- a/packages/fold-agent/test/Config/ConfigSchemaJson.vi.test.ts +++ b/packages/fold-agent/test/Config/ConfigSchemaJson.vi.test.ts @@ -44,7 +44,7 @@ it.effect('the starter config is valid against the schema (round-trips through t expect(config.roles.smart).toMatchObject({ provider: 'codex', - model: 'gpt-5.6-sol', + model: 'gpt-6-astra', reasoning: 'medium', }) expect(config.providers.codex?.kind).toBe('codex') diff --git a/packages/fold-agent/test/Config/ModelSelections.vi.test.ts b/packages/fold-agent/test/Config/ModelSelections.vi.test.ts index ce98c68..ac61736 100644 --- a/packages/fold-agent/test/Config/ModelSelections.vi.test.ts +++ b/packages/fold-agent/test/Config/ModelSelections.vi.test.ts @@ -76,7 +76,7 @@ it.effect('applies provider-specific role defaults around the directly selected expect(normal.root.activeModel.role).toBe('smart') expect(normal.smart.activeModel.modelId).toBe('chosen') expect(normal.fast.activeModel).toMatchObject({ providerId: 'codex', modelId: 'gpt-5.6-luna' }) - expect(normal.orchestrator.activeModel).toMatchObject({ providerId: 'codex', modelId: 'gpt-5.6-sol' }) + expect(normal.orchestrator.activeModel).toMatchObject({ providerId: 'codex', modelId: 'gpt-6-astra' }) expect(rlm.root.activeModel.modelId).toBe('chosen') expect(rlm.root.activeModel.role).toBe('orchestrator') expect(rlm.smart.activeModel).toMatchObject({ providerId: 'codex', modelId: 'gpt-5.6-terra' }) @@ -153,7 +153,7 @@ it.effect('describes profiles, credentials, and merged model candidates without expect(description.providers.find(({ name }) => name === 'codex')?.models).toEqual([ 'catalog-only', 'default-fast', - 'gpt-5.6-sol', + 'gpt-6-astra', 'profile-fast', 'profile-orchestrator', 'profile-smart', diff --git a/packages/fold-agent/test/Config/ProviderConfig.vi.test.ts b/packages/fold-agent/test/Config/ProviderConfig.vi.test.ts index 545d37f..daf2321 100644 --- a/packages/fold-agent/test/Config/ProviderConfig.vi.test.ts +++ b/packages/fold-agent/test/Config/ProviderConfig.vi.test.ts @@ -155,7 +155,7 @@ it.effect('adds OAuth profiles without an API key and supplies their default mod expect(updated.providers['work-codex']).toEqual({ kind: 'codex', baseUrl: 'https://example.test', - configuredModels: ['gpt-5.6-sol'], + configuredModels: ['gpt-6-astra'], }) }), ).pipe(Effect.provide(NodeFileSystem.layer)), diff --git a/packages/fold-agent/test/Mode/Launch.vi.test.ts b/packages/fold-agent/test/Mode/Launch.vi.test.ts index 8ae2b05..7692f4a 100644 --- a/packages/fold-agent/test/Mode/Launch.vi.test.ts +++ b/packages/fold-agent/test/Mode/Launch.vi.test.ts @@ -519,7 +519,7 @@ it.effect('mergeModelSelection drops the stale model when the newly named provid const config = yield* parseFoldConfig(mergeConfigText) // `fold --provider codex` alone: the anthropic model id must NOT ride onto codex - the binding - // comes back model-less so the codex default (gpt-5.6-sol) applies at resolution. + // comes back model-less so the codex default (gpt-6-astra) applies at resolution. const merged = mergeModelSelection(config, config.roles.smart, { provider: 'codex' }) expect(merged).toEqual({ provider: 'codex', reasoning: 'medium' }) expect(merged.model).toBeUndefined() diff --git a/packages/fold-cli/src/ContextWindow.ts b/packages/fold-cli/src/ContextWindow.ts new file mode 100644 index 0000000..f09445e --- /dev/null +++ b/packages/fold-cli/src/ContextWindow.ts @@ -0,0 +1,27 @@ +import { defaultContextWindowFor, type ActiveModel } from '@humanlayer/fold-core' + +const CODEX_EFFECTIVE_CONTEXT_WINDOW_PERCENT = 95 +const CODEX_BASELINE_TOKENS = 12_000 + +export const contextWindowLimitForDisplay = (model: ActiveModel, catalogContextWindow?: number): number => { + const rawLimit = + model.providerKind === 'codex' + ? defaultContextWindowFor(model.modelId) + : (catalogContextWindow ?? defaultContextWindowFor(model.modelId)) + + return model.providerKind === 'codex' + ? Math.floor((rawLimit * CODEX_EFFECTIVE_CONTEXT_WINDOW_PERCENT) / 100) + : rawLimit +} + +export const contextUsedPercentForDisplay = (used: number, model: ActiveModel, limit: number): number => { + if (model.providerKind !== 'codex') return Math.min(100, Math.round((used / limit) * 100)) + if (limit <= CODEX_BASELINE_TOKENS) return 100 + + const effectiveWindow = limit - CODEX_BASELINE_TOKENS + const effectiveUsed = Math.max(0, used - CODEX_BASELINE_TOKENS) + const remaining = Math.max(0, effectiveWindow - effectiveUsed) + const remainingPercent = Math.round((remaining / effectiveWindow) * 100) + + return 100 - remainingPercent +} diff --git a/packages/fold-cli/src/Renderer.ts b/packages/fold-cli/src/Renderer.ts index e1c694c..bfe4257 100644 --- a/packages/fold-cli/src/Renderer.ts +++ b/packages/fold-cli/src/Renderer.ts @@ -1,6 +1,5 @@ import { decodeBashOutputDelta } from '@humanlayer/fold-agent' import { - defaultContextWindowFor, lookupCatalogEntry, shortAgentId, usageCacheRead, @@ -21,6 +20,7 @@ import { import { Data, Effect, Match } from 'effect' import { makeAnsiPalette, type AnsiPalette } from './Ansi' +import { contextUsedPercentForDisplay, contextWindowLimitForDisplay } from './ContextWindow' type Writer = (text: string) => Effect.Effect @@ -249,8 +249,8 @@ const contextText = (usage: UsageEncoded, model: ActiveModel | null, entry: Mode const used = usageInputTotal(usage) + usageOutputTotal(usage) if (model === null) return formatInt(used) - const limit = entry?.contextWindow ?? defaultContextWindowFor(model.modelId) - const percent = Math.round((used / limit) * 100) + const limit = contextWindowLimitForDisplay(model, entry?.contextWindow) + const percent = contextUsedPercentForDisplay(used, model, limit) return `${formatInt(used)}/${formatInt(limit)} (${percent}%)` } diff --git a/packages/fold-cli/src/tui/TuiSessionWorkspace.ts b/packages/fold-cli/src/tui/TuiSessionWorkspace.ts index c76913d..a78f4a1 100644 --- a/packages/fold-cli/src/tui/TuiSessionWorkspace.ts +++ b/packages/fold-cli/src/tui/TuiSessionWorkspace.ts @@ -17,6 +17,7 @@ import { layerLiveIdFactory, lookupCatalogEntry, type SessionId, type FoldSessio import { Cause, Duration, Effect, type FileSystem, Match, Option, Scope } from 'effect' import { createSignal, type Accessor } from 'solid-js' +import { contextUsedPercentForDisplay, contextWindowLimitForDisplay } from '../ContextWindow' import { makeHostedTuiSession, type HostedTuiSession, type HostedTuiSessionMetadata } from './HostedTuiSession' import { requestToLaunchOptions, sessionToLaunchOptions } from './LaunchRequests' import { makeLiveSessionHost } from './LiveSessionHost' @@ -100,14 +101,19 @@ export const makeTuiSessionWorkspace = (options: { summary.model === null || options.tui.catalog === undefined ? null : lookupCatalogEntry(options.tui.catalog, summary.model) + const contextWindow = + summary.model === null || entry === null || entry.contextWindow <= 0 + ? null + : contextWindowLimitForDisplay(summary.model, entry.contextWindow) return { ...summary, contextPercent: - summary.contextTokens === null || entry === null || entry.contextWindow <= 0 + summary.contextTokens === null || summary.model === null || contextWindow === null ? null - : Math.min( - 100, - Math.round((summary.contextTokens / entry.contextWindow) * 100), + : contextUsedPercentForDisplay( + summary.contextTokens, + summary.model, + contextWindow, ), } }) diff --git a/packages/fold-cli/test/ContextWindow.vi.test.ts b/packages/fold-cli/test/ContextWindow.vi.test.ts new file mode 100644 index 0000000..36aef4b --- /dev/null +++ b/packages/fold-cli/test/ContextWindow.vi.test.ts @@ -0,0 +1,24 @@ +import type { ActiveModel } from '@humanlayer/fold-core' +import { expect, it } from 'vitest' + +import { contextUsedPercentForDisplay, contextWindowLimitForDisplay } from '../src/ContextWindow' + +const astra: ActiveModel = { + providerId: 'codex', + providerKind: 'codex' as const, + modelId: 'gpt-6-astra', + role: null, + requestedReasoningLevel: 'max' as const, + reasoning: { _tag: 'effort', effort: 'max', summary: 'auto' }, +} + +it('shows the Codex usable Astra window instead of the public API catalog window', () => { + expect(contextWindowLimitForDisplay(astra, 1_050_000)).toBe(258_400) +}) + +it('matches the Codex gauge percentage after excluding its baseline tokens', () => { + const limit = contextWindowLimitForDisplay(astra, 1_050_000) + + expect(contextUsedPercentForDisplay(12_000, astra, limit)).toBe(0) + expect(contextUsedPercentForDisplay(240_000, astra, limit)).toBe(93) +}) diff --git a/packages/fold-codex/src/CodexModel.ts b/packages/fold-codex/src/CodexModel.ts index eaf6ac8..dd714b2 100644 --- a/packages/fold-codex/src/CodexModel.ts +++ b/packages/fold-codex/src/CodexModel.ts @@ -45,7 +45,7 @@ export const CODEX_API_URL = 'https://chatgpt.com/backend-api/codex' export const DEFAULT_REQUEST_RETRY_TIMES = 3 /** The codex model used when {@link CodexModelOptions.model} is omitted. */ -export const DEFAULT_CODEX_MODEL_ID = 'gpt-5.6-sol' +export const DEFAULT_CODEX_MODEL_ID = 'gpt-6-astra' type ResponsesPayload = Omit type ResponseBody = typeof OpenAiSchema.Response.Type @@ -216,7 +216,7 @@ export const decorateCodexClient = (inner: OpenAiClient.Service, options: CodexR /** Options for {@link codexModel}. */ export type CodexModelOptions = { - /** Codex model id, for example `gpt-5.6-sol`. Defaults to {@link DEFAULT_CODEX_MODEL_ID}. */ + /** Codex model id, for example `gpt-6-astra`. Defaults to {@link DEFAULT_CODEX_MODEL_ID}. */ readonly model?: string /** Reasoning level for requests. Defaults to `off`, which leaves the backend default untouched. */ readonly reasoning?: ReasoningLevel diff --git a/packages/fold-codex/test/CodexModel.vi.test.ts b/packages/fold-codex/test/CodexModel.vi.test.ts index eed4219..a74036d 100644 --- a/packages/fold-codex/test/CodexModel.vi.test.ts +++ b/packages/fold-codex/test/CodexModel.vi.test.ts @@ -270,7 +270,7 @@ describe('codexModel defaults', () => { it('omitting the model binds the default codex model id', () => { const model = codexModel({}) - expect(DEFAULT_CODEX_MODEL_ID).toBe('gpt-5.6-sol') + expect(DEFAULT_CODEX_MODEL_ID).toBe('gpt-6-astra') expect(model.activeModel.modelId).toBe(DEFAULT_CODEX_MODEL_ID) expect(model.activeModel.providerKind).toBe('codex') }) diff --git a/packages/fold-core/src/Compaction/CompactionEngine.ts b/packages/fold-core/src/Compaction/CompactionEngine.ts index 9b1e807..f84407d 100644 --- a/packages/fold-core/src/Compaction/CompactionEngine.ts +++ b/packages/fold-core/src/Compaction/CompactionEngine.ts @@ -37,6 +37,7 @@ const filePartEstimateChars = 4_800 */ export const defaultModelContextWindows: ReadonlyArray = [ [/claude|fable|mythos/, 200_000], + [/gpt-6-astra/, 272_000], [/gpt-5|codex/, 272_000], [/gpt-4\.1/, 1_000_000], ] diff --git a/packages/fold-core/src/Compaction/CompactionLayer.ts b/packages/fold-core/src/Compaction/CompactionLayer.ts index 71c6e70..5696737 100644 --- a/packages/fold-core/src/Compaction/CompactionLayer.ts +++ b/packages/fold-core/src/Compaction/CompactionLayer.ts @@ -82,13 +82,14 @@ export const makeCompactionService = (config: EnabledAutoCompactConfig): Compact const turnPrefixOutputFraction = 0.5 /** - * Resolve the agent's context window: an explicit `autoCompact.contextWindow` always wins, then - * the session's ModelCatalog entry for the active model, then the interim pattern table (D15). - * The Reference default is the empty catalog, so this adds nothing to the effect requirements. + * Resolve the agent's context window: an explicit `autoCompact.contextWindow` always wins. Codex + * uses its app-specific window because the public OpenAI API catalog can advertise a larger limit; + * other providers use the session ModelCatalog before the interim pattern table (D15). */ const contextWindowFor = (input: CompactionCheckInput): Effect.Effect => Effect.gen(function* () { if (config.contextWindow !== undefined) return config.contextWindow + if (input.model?.providerKind === 'codex') return defaultContextWindowFor(input.model.modelId) const entry = input.model === null ? null : yield* (yield* ModelCatalog).lookup(input.model) diff --git a/packages/fold-core/test/Compaction/AutoCompactSession.vi.test.ts b/packages/fold-core/test/Compaction/AutoCompactSession.vi.test.ts index 1013f30..ac3575c 100644 --- a/packages/fold-core/test/Compaction/AutoCompactSession.vi.test.ts +++ b/packages/fold-core/test/Compaction/AutoCompactSession.vi.test.ts @@ -576,6 +576,35 @@ it.effect('a session-provided catalog supplies the compaction context window (no }).pipe(Effect.scoped, Effect.provide(NodeFileSystem.layer)), ) +it.effect('a Codex model uses the app context window instead of the public API catalog window', () => + Effect.gen(function* () { + const astraModel = { ...codexActiveModel, modelId: 'gpt-6-astra' } + const { model, scripted } = yield* scriptedModel(astraModel, [ + textTurn(`noted ${'a'.repeat(120)}`, { inputTokens: 240_000 }), + textTurn('## Goal\n- codex-window summary'), + textTurn('post-codex-compaction answer'), + ]) + + const session = yield* startSession({ + agent: defineAgent({ model, autoCompact: { enabled: true, keepRecentTokens: 10 } }), + catalog: [ + { + ...scriptedCatalogEntry(1_050_000), + providerId: 'openai', + modelId: 'gpt-6-astra', + }, + ], + }) + + yield* session.send('codex topic one') + const finished = yield* session.send('codex topic two') + + expect(finished.resultText).toBe('post-codex-compaction 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 9863ef8..adc9ae9 100644 --- a/packages/fold-core/test/Compaction/CompactionEngine.vi.test.ts +++ b/packages/fold-core/test/Compaction/CompactionEngine.vi.test.ts @@ -147,6 +147,7 @@ it('defaultContextWindowFor uses the interim table with a conservative fallback' expect(defaultContextWindowFor('claude-fable-5')).toBe(200_000) 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('some-unknown-model')).toBe(128_000) expect(defaultContextWindowFor(null)).toBe(128_000) })