blob: a64fc424e2af5416ede2285b98c47bf13568ed7d [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import assert from 'node:assert/strict';
import { mkdtemp, readFile, readdir, rm, stat, utimes, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { test } from 'node:test';
import { loadTypeScriptModule, main, PROVIDERS } from './sync-model-metadata.mjs';
function fixtureCatalog() {
const catalog = {};
for (const sourceId of new Set(Object.values(PROVIDERS))) {
catalog[sourceId] = {
id: sourceId,
name: `Provider ${sourceId}`,
doc: `https://example.test/${sourceId}`,
models: {
model: {
name: 'Model',
limit: { context: 1024, output: 128 },
reasoning: false,
tool_call: true,
cost: { input: 1, output: 2 },
},
},
};
}
catalog.unused = { id: 'unused', name: 'Unused', doc: 'https://example.test/unused', models: {} };
return catalog;
}
test('the committed snapshot generates both TypeScript modules offline', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-model-build-'));
try {
const metadata = join(root, 'metadata.ts');
const pricing = join(root, 'pricing.ts');
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => assert.fail('offline generation must not fetch');
try {
await main([
'node',
'sync-model-metadata.mjs',
'--output',
metadata,
'--pricing-output',
pricing,
]);
} finally {
globalThis.fetch = originalFetch;
}
const [metadataSource, pricingSource] = await Promise.all([
readFile(metadata, 'utf8'),
readFile(pricing, 'utf8'),
]);
const [metadataModule, pricingModule] = await Promise.all([
loadTypeScriptModule(metadataSource),
loadTypeScriptModule(pricingSource),
]);
assert.ok(Object.keys(metadataModule.GENERATED_MODELS_DEV_METADATA).length > 0);
assert.ok(Array.isArray(pricingModule.GENERATED_MODEL_PRICING));
assert.ok(pricingModule.GENERATED_MODEL_PRICING.length > 0);
} finally {
await rm(root, { recursive: true, force: true });
}
});
test('a no-op sync preserves generated output mtimes', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-model-noop-'));
try {
const metadata = join(root, 'metadata.ts');
const pricing = join(root, 'pricing.ts');
const argv = [
'node',
'sync-model-metadata.mjs',
'--output',
metadata,
'--pricing-output',
pricing,
];
await main(argv);
const oldTime = new Date('2001-01-01T00:00:00.000Z');
await Promise.all([utimes(metadata, oldTime, oldTime), utimes(pricing, oldTime, oldTime)]);
const before = await Promise.all([stat(metadata), stat(pricing)]);
await main(argv);
const after = await Promise.all([stat(metadata), stat(pricing)]);
assert.deepEqual(
after.map(({ mtimeMs }) => mtimeMs),
before.map(({ mtimeMs }) => mtimeMs),
);
} finally {
await rm(root, { recursive: true, force: true });
}
});
test('a refresh persists the exact selected input and check fails on stale output', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-model-snapshot-'));
try {
const input = join(root, 'api.json');
const snapshot = join(root, 'snapshot.json');
const metadata = join(root, 'metadata.ts');
const pricing = join(root, 'pricing.ts');
await writeFile(input, JSON.stringify(fixtureCatalog()));
await main([
'node',
'sync-model-metadata.mjs',
'--refresh',
'--refresh-input',
input,
'--snapshot',
snapshot,
'--output',
metadata,
'--pricing-output',
pricing,
]);
const persisted = JSON.parse(await readFile(snapshot, 'utf8'));
assert.equal(persisted.formatVersion, 1);
assert.equal(persisted.origin.kind, 'models-dev-response');
assert.equal(persisted.projection.metadata.unused, undefined);
assert.deepEqual(
Object.keys(persisted.projection.metadata).sort(),
Object.keys(PROVIDERS).sort(),
);
assert.match(await readFile(metadata, 'utf8'), new RegExp(persisted.projectionSha256));
await main([
'node',
'sync-model-metadata.mjs',
'--check',
'--snapshot',
snapshot,
'--output',
metadata,
'--pricing-output',
pricing,
]);
const corrected = fixtureCatalog();
corrected.anthropic.models.model.name = 'Corrected Model';
corrected.anthropic.models.model.cost.input = 1.5;
await writeFile(input, JSON.stringify(corrected));
await main([
'node',
'sync-model-metadata.mjs',
'--refresh',
'--refresh-input',
input,
'--snapshot',
snapshot,
'--output',
metadata,
'--pricing-output',
pricing,
]);
const correctedProjection = JSON.parse(await readFile(snapshot, 'utf8')).projection;
assert.equal(correctedProjection.metadata.anthropic.model.displayName, 'Corrected Model');
assert.equal(
correctedProjection.pricing.find((entry) => entry.modelKey === 'anthropic:model')
.inputUsdPer1M,
1.5,
);
await writeFile(
metadata,
(await readFile(metadata, 'utf8')).replace(
'"displayName":"Corrected Model"',
'"displayName":"Stale"',
),
);
await assert.rejects(
main([
'node',
'sync-model-metadata.mjs',
'--check',
'--snapshot',
snapshot,
'--output',
metadata,
'--pricing-output',
pricing,
]),
/is stale/,
);
} finally {
await rm(root, { recursive: true, force: true });
}
});
test('a modified snapshot fails closed before generation', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-model-snapshot-tamper-'));
try {
const input = join(root, 'api.json');
const snapshot = join(root, 'snapshot.json');
const metadata = join(root, 'metadata.ts');
await writeFile(input, JSON.stringify(fixtureCatalog()));
await main([
'node',
'sync-model-metadata.mjs',
'--refresh',
'--refresh-input',
input,
'--snapshot',
snapshot,
'--output',
metadata,
]);
const persisted = JSON.parse(await readFile(snapshot, 'utf8'));
persisted.projection.metadata.anthropic.model.displayName = 'tampered';
await writeFile(snapshot, JSON.stringify(persisted));
await assert.rejects(
main(['node', 'sync-model-metadata.mjs', '--snapshot', snapshot, '--output', metadata]),
/digest mismatch/,
);
} finally {
await rm(root, { recursive: true, force: true });
}
});
test('refresh rejects an empty required provider before replacing outputs', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-model-snapshot-empty-provider-'));
try {
const input = join(root, 'api.json');
const snapshot = join(root, 'snapshot.json');
const metadata = join(root, 'metadata.ts');
const catalog = fixtureCatalog();
catalog.anthropic.models = {};
await writeFile(input, JSON.stringify(catalog));
await writeFile(snapshot, 'old snapshot');
await writeFile(metadata, 'old metadata');
await assert.rejects(
main([
'node',
'sync-model-metadata.mjs',
'--refresh',
'--refresh-input',
input,
'--snapshot',
snapshot,
'--output',
metadata,
]),
/provider anthropic has no non-empty models object/,
);
assert.equal(await readFile(snapshot, 'utf8'), 'old snapshot');
assert.equal(await readFile(metadata, 'utf8'), 'old metadata');
} finally {
await rm(root, { recursive: true, force: true });
}
});
test('refresh rejects unknown model modalities instead of dropping them', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-model-snapshot-modalities-'));
try {
const input = join(root, 'api.json');
const snapshot = join(root, 'snapshot.json');
const metadata = join(root, 'metadata.ts');
const catalog = fixtureCatalog();
catalog.anthropic.models.model.modalities = { input: ['text', 'hologram'], output: ['text'] };
await writeFile(input, JSON.stringify(catalog));
await assert.rejects(
main([
'node',
'sync-model-metadata.mjs',
'--refresh',
'--refresh-input',
input,
'--snapshot',
snapshot,
'--output',
metadata,
]),
/unsupported modalities/,
);
} finally {
await rm(root, { recursive: true, force: true });
}
});
// The drift report goes to stdout for the scheduled job's summary; tests read
// the returned buckets instead of the printed text.
async function drift(argv) {
const written = [];
const original = process.stdout.write;
process.stdout.write = (chunk) => {
written.push(String(chunk));
return true;
};
try {
return { report: await main(['node', 'sync-model-metadata.mjs', '--drift', ...argv]) };
} finally {
process.stdout.write = original;
assert.ok(written.join('').length > 0, 'drift must print a report');
}
}
test('drift finds nothing when the snapshot still matches upstream', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-model-drift-clean-'));
try {
const input = join(root, 'api.json');
const snapshot = join(root, 'snapshot.json');
await writeFile(input, JSON.stringify(fixtureCatalog()));
await main([
'node',
'sync-model-metadata.mjs',
'--refresh',
'--refresh-input',
input,
'--snapshot',
snapshot,
'--output',
join(root, 'metadata.ts'),
]);
const { report } = await drift(['--snapshot', snapshot, '--refresh-input', input]);
assert.equal(report.drifted, false);
assert.deepEqual(
[
report.added,
report.removed,
report.changed,
report.rejectedModels,
report.rejectedProviders,
],
[[], [], [], [], []],
);
} finally {
await rm(root, { recursive: true, force: true });
}
});
test('drift separates a rejected shape from a real upstream difference', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-model-drift-'));
try {
const committed = join(root, 'api.json');
const upstream = join(root, 'upstream.json');
const snapshot = join(root, 'snapshot.json');
const before = fixtureCatalog();
before.anthropic.models.legacy = { ...before.anthropic.models.model, name: 'Legacy' };
await writeFile(committed, JSON.stringify(before));
await main([
'node',
'sync-model-metadata.mjs',
'--refresh',
'--refresh-input',
committed,
'--snapshot',
snapshot,
'--output',
join(root, 'metadata.ts'),
]);
const after = JSON.parse(JSON.stringify(before));
delete after.anthropic.models.legacy;
after.anthropic.models.added = { ...after.anthropic.models.model, name: 'Added' };
after.anthropic.models.model.name = 'Renamed';
after.anthropic.models.model.cost = { input: 9, output: 9 };
after.groq.models.model.modalities = { input: ['text', 'hologram'], output: ['text'] };
delete after.openai;
await writeFile(upstream, JSON.stringify(after));
const { report } = await drift(['--snapshot', snapshot, '--refresh-input', upstream]);
assert.equal(report.drifted, true);
assert.deepEqual(report.added, ['anthropic/added']);
assert.deepEqual(report.removed, ['anthropic/legacy']);
assert.deepEqual(report.changed, [
'anthropic/model: metadata.displayName, pricing.inputUsdPer1M, pricing.outputUsdPer1M',
]);
assert.equal(report.rejectedProviders.length, 1);
assert.match(report.rejectedProviders[0], /^openai: .*provider openai is missing/u);
assert.equal(report.rejectedModels.length, 1);
assert.match(report.rejectedModels[0], /^groq\/model: .*unsupported modalities/u);
} finally {
await rm(root, { recursive: true, force: true });
}
});
test('drift reports the projection sections that carry no model metadata', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-model-drift-sections-'));
try {
const committed = join(root, 'api.json');
const upstream = join(root, 'upstream.json');
const snapshot = join(root, 'snapshot.json');
const before = fixtureCatalog();
before.anthropic.models.model.provider = { npm: '@ai-sdk/anthropic' };
await writeFile(committed, JSON.stringify(before));
await main([
'node',
'sync-model-metadata.mjs',
'--refresh',
'--refresh-input',
committed,
'--snapshot',
snapshot,
'--output',
join(root, 'metadata.ts'),
]);
// Neither a renamed provider nor a swapped npm package touches a model's
// metadata or its pricing, which is all the report used to compare.
const after = JSON.parse(JSON.stringify(before));
after.anthropic.name = 'Anthropic Renamed';
after.anthropic.models.model.provider = { npm: '@ai-sdk/google' };
await writeFile(upstream, JSON.stringify(after));
const { report } = await drift(['--snapshot', snapshot, '--refresh-input', upstream]);
assert.equal(report.drifted, true);
assert.deepEqual(report.changed, [
'anthropic: providerFacts.name',
'anthropic/model: providerOverrides.npm',
]);
} finally {
await rm(root, { recursive: true, force: true });
}
});
test('drift refuses to combine with the modes that write', async () => {
for (const mode of ['--refresh', '--check']) {
await assert.rejects(
main(['node', 'sync-model-metadata.mjs', '--drift', mode]),
/--drift reports without writing/u,
);
}
});
test('refresh carries the video and pdf modalities models.dev declares', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-model-snapshot-video-'));
try {
const input = join(root, 'api.json');
const snapshot = join(root, 'snapshot.json');
const metadata = join(root, 'metadata.ts');
const catalog = fixtureCatalog();
const modalities = { input: ['text', 'video'], output: ['text', 'pdf', 'video'] };
catalog.anthropic.models.model.modalities = modalities;
await writeFile(input, JSON.stringify(catalog));
await main([
'node',
'sync-model-metadata.mjs',
'--refresh',
'--refresh-input',
input,
'--snapshot',
snapshot,
'--output',
metadata,
]);
const written = JSON.parse(await readFile(snapshot, 'utf8'));
assert.deepEqual(written.projection.metadata.anthropic.model.modalities, modalities);
} finally {
await rm(root, { recursive: true, force: true });
}
});
test('refresh rejects a partial provider shrink until it is explicitly accepted', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-model-snapshot-provider-shrink-'));
try {
const input = join(root, 'api.json');
const snapshot = join(root, 'snapshot.json');
const metadata = join(root, 'metadata.ts');
const pricing = join(root, 'pricing.ts');
const initial = fixtureCatalog();
initial.anthropic.models['model-two'] = {
name: 'Model Two',
limit: { context: 2048, output: 256 },
reasoning: true,
tool_call: true,
cost: { input: 3, output: 4 },
};
await writeFile(input, JSON.stringify(initial));
const refreshArgs = [
'node',
'sync-model-metadata.mjs',
'--refresh',
'--refresh-input',
input,
'--snapshot',
snapshot,
'--output',
metadata,
'--pricing-output',
pricing,
];
await main(refreshArgs);
const before = {
snapshot: await readFile(snapshot, 'utf8'),
metadata: await readFile(metadata, 'utf8'),
pricing: await readFile(pricing, 'utf8'),
};
await writeFile(input, JSON.stringify(fixtureCatalog()));
await assert.rejects(main(refreshArgs), /projection paths: \/metadata\/anthropic\/model-two/);
assert.deepEqual(
{
snapshot: await readFile(snapshot, 'utf8'),
metadata: await readFile(metadata, 'utf8'),
pricing: await readFile(pricing, 'utf8'),
},
before,
);
await main([...refreshArgs, '--accept-upstream-removals']);
const accepted = JSON.parse(await readFile(snapshot, 'utf8'));
assert.equal(accepted.projection.metadata.anthropic['model-two'], undefined);
} finally {
await rm(root, { recursive: true, force: true });
}
});
test('refresh rejects lost pricing coverage from an otherwise valid model', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-model-snapshot-pricing-shrink-'));
try {
const input = join(root, 'api.json');
const snapshot = join(root, 'snapshot.json');
const metadata = join(root, 'metadata.ts');
const pricing = join(root, 'pricing.ts');
const refreshArgs = [
'node',
'sync-model-metadata.mjs',
'--refresh',
'--refresh-input',
input,
'--snapshot',
snapshot,
'--output',
metadata,
'--pricing-output',
pricing,
];
await writeFile(input, JSON.stringify(fixtureCatalog()));
await main(refreshArgs);
const before = {
snapshot: await readFile(snapshot, 'utf8'),
metadata: await readFile(metadata, 'utf8'),
pricing: await readFile(pricing, 'utf8'),
};
const truncated = fixtureCatalog();
delete truncated.anthropic.models.model.cost;
await writeFile(input, JSON.stringify(truncated));
await assert.rejects(main(refreshArgs), /projection paths: \/pricing\/anthropic:model/);
assert.deepEqual(
{
snapshot: await readFile(snapshot, 'utf8'),
metadata: await readFile(metadata, 'utf8'),
pricing: await readFile(pricing, 'utf8'),
},
before,
);
} finally {
await rm(root, { recursive: true, force: true });
}
});
test('refresh rejects nested capability and pricing shrinkage', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-model-snapshot-nested-shrink-'));
try {
const input = join(root, 'api.json');
const snapshot = join(root, 'snapshot.json');
const metadata = join(root, 'metadata.ts');
const pricing = join(root, 'pricing.ts');
const refreshArgs = [
'node',
'sync-model-metadata.mjs',
'--refresh',
'--refresh-input',
input,
'--snapshot',
snapshot,
'--output',
metadata,
'--pricing-output',
pricing,
];
const initial = fixtureCatalog();
initial.anthropic.models.model.modalities = {
input: ['text', 'image'],
output: ['text'],
};
initial.anthropic.models.model.reasoning = true;
initial.anthropic.models.model.cost.cache_read = 0.25;
await writeFile(input, JSON.stringify(initial));
await main(refreshArgs);
const before = {
snapshot: await readFile(snapshot, 'utf8'),
metadata: await readFile(metadata, 'utf8'),
pricing: await readFile(pricing, 'utf8'),
};
const truncated = structuredClone(initial);
truncated.anthropic.models.model.modalities.input = ['text'];
delete truncated.anthropic.models.model.cost.cache_read;
await writeFile(input, JSON.stringify(truncated));
await assert.rejects(
main(refreshArgs),
(error) =>
/\/metadata\/anthropic\/model\/modalities\/input value "image"/.test(error.message) &&
/\/pricing\/anthropic:model\/cacheReadUsdPer1M/.test(error.message),
);
assert.deepEqual(
{
snapshot: await readFile(snapshot, 'utf8'),
metadata: await readFile(metadata, 'utf8'),
pricing: await readFile(pricing, 'utf8'),
},
before,
);
for (const [sourceField, projectionField] of [
['reasoning', 'reasoning'],
['tool_call', 'functionCalling'],
]) {
const capabilityLoss = structuredClone(initial);
capabilityLoss.anthropic.models.model[sourceField] = false;
await writeFile(input, JSON.stringify(capabilityLoss));
await assert.rejects(
main(refreshArgs),
new RegExp(`/metadata/anthropic/model/capabilities/${projectionField}`),
);
assert.deepEqual(
{
snapshot: await readFile(snapshot, 'utf8'),
metadata: await readFile(metadata, 'utf8'),
pricing: await readFile(pricing, 'utf8'),
},
before,
);
}
const accepted = structuredClone(initial);
accepted.anthropic.models.model.tool_call = false;
await writeFile(input, JSON.stringify(accepted));
await main([...refreshArgs, '--accept-upstream-removals']);
assert.equal(
JSON.parse(await readFile(snapshot, 'utf8')).projection.metadata.anthropic.model.capabilities
.functionCalling,
false,
);
} finally {
await rm(root, { recursive: true, force: true });
}
});
test('refresh leaves all targets unchanged when any output cannot be staged', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-model-snapshot-transaction-'));
try {
const input = join(root, 'api.json');
const snapshot = join(root, 'snapshot.json');
const metadata = join(root, 'metadata.ts');
const pricing = join(root, 'pricing.ts');
const missingPricing = join(root, 'missing', 'pricing.ts');
await writeFile(input, JSON.stringify(fixtureCatalog()));
await main([
'node',
'sync-model-metadata.mjs',
'--refresh',
'--refresh-input',
input,
'--snapshot',
snapshot,
'--output',
metadata,
'--pricing-output',
pricing,
]);
const before = {
snapshot: await readFile(snapshot, 'utf8'),
metadata: await readFile(metadata, 'utf8'),
};
await assert.rejects(
main([
'node',
'sync-model-metadata.mjs',
'--refresh',
'--refresh-input',
input,
'--snapshot',
snapshot,
'--output',
metadata,
'--pricing-output',
missingPricing,
]),
/ENOENT/,
);
assert.equal(await readFile(snapshot, 'utf8'), before.snapshot);
assert.equal(await readFile(metadata, 'utf8'), before.metadata);
await assert.rejects(readFile(missingPricing), /ENOENT/);
assert.deepEqual((await readdir(root)).sort(), [
'api.json',
'metadata.ts',
'pricing.ts',
'snapshot.json',
]);
} finally {
await rm(root, { recursive: true, force: true });
}
});