diff --git a/src/lib/review.js b/src/lib/review.js index 27f2fd8..b8b3d19 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -105,6 +105,7 @@ export async function runReview({ cfg, gitea, adminGitea, db, job, withGlobal, l tenantId: cfg.tenantId, timeoutMs: cfg.routerTimeoutMs }); servedModel = out.servedModel; + assertServedModelAllowed(cfg.model, servedModel); const raw = extractJson(out.content); if (!raw) { log(`[${fullRepo}@${sha.slice(0, 8)}] unparseable model output (served=${servedModel}): ${JSON.stringify(out.content.slice(0, 400))}`); @@ -313,6 +314,17 @@ export async function fetchDiff({ gitea, job, log = () => {}, maxBytes = Infinit throw allDiffSourcesFailed(tried); } +export function assertServedModelAllowed(requestedModel, servedModel) { + if (!isLocalModel(requestedModel)) return; + if (!servedModel) return; + if (servedModel === requestedModel) return; + throw new Error(`local model contract violated: requested ${requestedModel}, served ${servedModel}`); +} + +function isLocalModel(model) { + return /^ollama(?:-remote)?\//.test(String(model || '')); +} + function allDiffSourcesFailed(tried) { let msg = tried.join(' | '); if (msg.length > 1500) msg = msg.slice(0, 1500) + ` …(+${tried.length} sources)`; diff --git a/test/modelContract.test.js b/test/modelContract.test.js new file mode 100644 index 0000000..a8dfad5 --- /dev/null +++ b/test/modelContract.test.js @@ -0,0 +1,25 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { assertServedModelAllowed } from '../src/lib/review.js'; + +test('local model reviews accept the exact served local model', () => { + assert.doesNotThrow(() => + assertServedModelAllowed('ollama-remote/qwen2.5-coder:7b', 'ollama-remote/qwen2.5-coder:7b')); +}); + +test('local model reviews fail on cloud fallback drift', () => { + assert.throws( + () => assertServedModelAllowed('ollama-remote/qwen2.5-coder:7b', 'google/gemini-2.5-flash'), + /local model contract violated/ + ); +}); + +test('cloud model reviews allow provider fallback reporting', () => { + assert.doesNotThrow(() => + assertServedModelAllowed('anthropic/claude-sonnet-4-6', 'google/gemini-2.5-flash')); +}); + +test('missing served model metadata does not fail local requests', () => { + assert.doesNotThrow(() => + assertServedModelAllowed('ollama-remote/qwen2.5-coder:7b', null)); +});