diff --git a/packages/fold-agent/scripts/update-model-catalog.ts b/packages/fold-agent/scripts/update-model-catalog.ts index 6bbbe20..a19517a 100644 --- a/packages/fold-agent/scripts/update-model-catalog.ts +++ b/packages/fold-agent/scripts/update-model-catalog.ts @@ -1,6 +1,6 @@ /** - * Regenerates `src/Catalog/BakedCatalog.ts` - the baked models.dev snapshot for the `anthropic` and - * `openai` providers, already normalized to core `ModelCatalogEntry` rows. Reads a local snapshot + * Regenerates `src/Catalog/BakedCatalog.ts` - the baked models.dev snapshot for the `anthropic`, + * `openai`, and `xai` providers, normalized to core `ModelCatalogEntry` rows. Reads a local snapshot * from `/tmp/models_dev_api.json` when present (avoid hammering models.dev while iterating), * otherwise fetches `https://models.dev/api.json` fresh. * @@ -19,7 +19,7 @@ import { modelCatalogEntriesFromModelsDev } from '../src/Catalog/Normalize' const SNAPSHOT_PATH = '/tmp/models_dev_api.json' const MODELS_DEV_URL = 'https://models.dev/api.json' -const BAKED_PROVIDERS: ReadonlySet = new Set(['anthropic', 'openai']) +const BAKED_PROVIDERS: ReadonlySet = new Set(['anthropic', 'openai', 'xai']) const scriptDir = dirname(fileURLToPath(import.meta.url)) const targetPath = join(scriptDir, '..', 'src', 'Catalog', 'BakedCatalog.ts') @@ -85,14 +85,14 @@ const entrySource = (entry: ModelCatalogEntry): string => const fileSource = (entries: ReadonlyArray): string => [ '// GENERATED by scripts/update-model-catalog.ts - do not edit by hand.', - '// Baked models.dev snapshot for the anthropic and openai providers, normalized to ModelCatalogEntry.', + '// Baked models.dev snapshot for the anthropic, openai, and xai providers, normalized to ModelCatalogEntry.', '// Regenerate: bun packages/fold-agent/scripts/update-model-catalog.ts', "import type { ModelCatalogEntry } from '@humanlayer/fold-core'", '', '/**', ' * The offline model catalog: the last-resort fallback when neither a cached nor a freshly fetched', - ' * models.dev catalog is available (D15). Covers every anthropic and openai model models.dev knew', - ' * at generation time.', + ' * models.dev catalog is available (D15). Covers every anthropic, openai, and xai model models.dev', + ' * knew at generation time.', ' */', 'export const bakedModelCatalog: ReadonlyArray = [', ...entries.map(entrySource), @@ -112,7 +112,7 @@ const main = Effect.gen(function* () { if (entries.length === 0) { return yield* new CatalogBakeError({ - message: 'no anthropic/openai entries survived normalization; refusing to bake', + message: 'no anthropic/openai/xai entries survived normalization; refusing to bake', }) } diff --git a/packages/fold-agent/src/Catalog/BakedCatalog.ts b/packages/fold-agent/src/Catalog/BakedCatalog.ts index 80e275b..9fb45d3 100644 --- a/packages/fold-agent/src/Catalog/BakedCatalog.ts +++ b/packages/fold-agent/src/Catalog/BakedCatalog.ts @@ -1,12 +1,12 @@ // GENERATED by scripts/update-model-catalog.ts - do not edit by hand. -// Baked models.dev snapshot for the anthropic and openai providers, normalized to ModelCatalogEntry. +// Baked models.dev snapshot for the anthropic, openai, and xai providers, normalized to ModelCatalogEntry. // Regenerate: bun packages/fold-agent/scripts/update-model-catalog.ts import type { ModelCatalogEntry } from '@humanlayer/fold-core' /** * The offline model catalog: the last-resort fallback when neither a cached nor a freshly fetched - * models.dev catalog is available (D15). Covers every anthropic and openai model models.dev knew - * at generation time. + * models.dev catalog is available (D15). Covers every anthropic, openai, and xai model models.dev + * knew at generation time. */ export const bakedModelCatalog: ReadonlyArray = [ { @@ -1001,4 +1001,30 @@ export const bakedModelCatalog: ReadonlyArray = [ toolCall: false, pricing: { inputPerMTokens: 0.1, outputPerMTokens: 0, cacheReadPerMTokens: null, cacheWritePerMTokens: null }, }, + { + providerId: 'xai', + modelId: 'grok-4.5', + name: 'Grok 4.5', + contextWindow: 500000, + maxInputTokens: null, + maxOutputTokens: 500000, + reasoning: true, + reasoningEfforts: ['low', 'medium', 'high'], + vision: true, + toolCall: true, + pricing: { inputPerMTokens: 2, outputPerMTokens: 6, cacheReadPerMTokens: 0.3, cacheWritePerMTokens: null }, + }, + { + providerId: 'xai', + modelId: 'grok-4.6', + name: 'Grok 4.6', + contextWindow: 500000, + maxInputTokens: null, + maxOutputTokens: 500000, + reasoning: true, + reasoningEfforts: ['low', 'medium', 'high', 'xhigh'], + vision: true, + toolCall: true, + pricing: { inputPerMTokens: 2, outputPerMTokens: 6, cacheReadPerMTokens: 0.5, cacheWritePerMTokens: null }, + }, ] diff --git a/packages/fold-agent/test/Catalog/BakedCatalog.vi.test.ts b/packages/fold-agent/test/Catalog/BakedCatalog.vi.test.ts index 1f9427e..8ba8b9a 100644 --- a/packages/fold-agent/test/Catalog/BakedCatalog.vi.test.ts +++ b/packages/fold-agent/test/Catalog/BakedCatalog.vi.test.ts @@ -28,6 +28,15 @@ const openAiTerra: ActiveModel = { reasoning: { _tag: 'effort', effort: 'max' }, } +const xaiGrok: ActiveModel = { + providerId: 'xai', + providerKind: 'openai-compatible', + modelId: 'grok-4.6', + role: null, + requestedReasoningLevel: 'xhigh', + reasoning: { _tag: 'effort', effort: 'xhigh' }, +} + it('resolves a codex-kind gpt-5.6-sol to the baked openai entry', () => { const entry = lookupCatalogEntry(bakedModelCatalog, codexSol) @@ -51,3 +60,15 @@ it('resolves an openai-compatible gpt-5.6-terra to the baked openai entry', () = expect(entry?.pricing?.outputPerMTokens).toBe(15) expect(entry?.reasoningEfforts).toContain('max') }) + +it('resolves the supported Grok model to the baked xAI entry', () => { + const entry = lookupCatalogEntry(bakedModelCatalog, xaiGrok) + + expect(entry).not.toBeNull() + expect(entry?.providerId).toBe('xai') + expect(entry?.modelId).toBe('grok-4.6') + expect(entry?.contextWindow).toBe(500_000) + expect(entry?.pricing?.inputPerMTokens).toBe(2) + expect(entry?.pricing?.outputPerMTokens).toBe(6) + expect(entry?.reasoningEfforts).toContain('xhigh') +})