From 48dff2078ee6f1117fff0841aee07feef6a612cb Mon Sep 17 00:00:00 2001 From: sax3l Date: Mon, 2 Nov 2026 16:35:00 +0100 Subject: [PATCH] fix(admission): zero maximumCredits means pilot mode, not a missing reservation The structure route ran the model with maximumCredits=1 and no reservation when credits were off, so admission returned reservation_required and the route answered 502. Admission now requires a reservation only when maximumCredits>0. Verified against the live stack: DOC0 segments -> LiteLLM/Ollama (inf0-valuation) -> evidence validation -> needs_review (no export). --- app/api/0pt/documents/structure/route.ts | 2 +- src/document-workbench/model-admission.ts | 15 +++++++++++---- tests/document-workbench/model-admission.test.ts | 8 ++++++++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/app/api/0pt/documents/structure/route.ts b/app/api/0pt/documents/structure/route.ts index 53ffe79..7c54fc6 100644 --- a/app/api/0pt/documents/structure/route.ts +++ b/app/api/0pt/documents/structure/route.ts @@ -306,7 +306,7 @@ export async function POST(request: NextRequest) { dataClass, maxInputTokens: policy.maxInputTokens, maxOutputTokens: policy.maxOutputTokens, - maximumCredits: Math.max(1, maximumCredits), + maximumCredits, }, policy: { tenantId, diff --git a/src/document-workbench/model-admission.ts b/src/document-workbench/model-admission.ts index 80f046f..8ac24f2 100644 --- a/src/document-workbench/model-admission.ts +++ b/src/document-workbench/model-admission.ts @@ -47,8 +47,11 @@ export function admitModelExecution( || !Array.isArray(policy.allowedModelIds) || !Array.isArray(policy.allowedDataClasses) || !['none', 'low', 'medium', 'high', 'restricted'].includes(request.dataClass) || !['internal', 'external'].includes(policy.destination) || typeof policy.externalTransferAllowed !== 'boolean' - || ![request.maxInputTokens, request.maxOutputTokens, request.maximumCredits, policy.maxInputTokens, policy.maxOutputTokens] - .every((n) => Number.isSafeInteger(n) && n > 0)) return { allowed: false, reason: 'invalid_request' }; + || ![request.maxInputTokens, request.maxOutputTokens, policy.maxInputTokens, policy.maxOutputTokens] + .every((n) => Number.isSafeInteger(n) && n > 0) + || !Number.isSafeInteger(request.maximumCredits) || request.maximumCredits < 0) { + return { allowed: false, reason: 'invalid_request' }; + } if (request.tenantId !== policy.tenantId || request.projectId !== policy.projectId) return { allowed: false, reason: 'scope_mismatch' }; if (!policy.allowedModelIds.includes(request.modelId)) return { allowed: false, reason: 'model_not_allowed' }; if (!policy.allowedDataClasses.includes(request.dataClass)) return { allowed: false, reason: 'data_class_not_allowed' }; @@ -58,9 +61,13 @@ export function admitModelExecution( if (request.maxInputTokens > policy.maxInputTokens || request.maxOutputTokens > policy.maxOutputTokens) { return { allowed: false, reason: 'token_limit_exceeded' }; } - if (!reservation || reservation.tenantId !== request.tenantId || reservation.projectId !== request.projectId + // maximumCredits === 0 means the operator runs this route without a wallet + // (explicit pilot mode), so no reservation is required. Any positive amount + // still requires an active, matching reservation. + if (request.maximumCredits > 0 && ( + !reservation || reservation.tenantId !== request.tenantId || reservation.projectId !== request.projectId || reservation.jobId !== request.jobId || reservation.status !== 'reserved' - || !Number.isSafeInteger(reservation.credits) || reservation.credits < request.maximumCredits) { + || !Number.isSafeInteger(reservation.credits) || reservation.credits < request.maximumCredits)) { return { allowed: false, reason: 'reservation_required' }; } return { allowed: true }; diff --git a/tests/document-workbench/model-admission.test.ts b/tests/document-workbench/model-admission.test.ts index c71d424..cabd8ee 100644 --- a/tests/document-workbench/model-admission.test.ts +++ b/tests/document-workbench/model-admission.test.ts @@ -18,6 +18,14 @@ describe('model execution admission', () => { expect(admitModelExecution(request, policy, { ...hold, status: 'settled' })).toEqual({ allowed: false, reason: 'reservation_required' }); expect(admitModelExecution(request, policy, { ...hold, jobId: 'other' })).toEqual({ allowed: false, reason: 'reservation_required' }); }); + it('pilot mode (maximumCredits 0) requires no reservation but still enforces every other rule', () => { + const pilot = { ...request, maximumCredits: 0 }; + expect(admitModelExecution(pilot, policy, undefined)).toEqual({ allowed: true }); + expect(admitModelExecution(pilot, { ...policy, allowedModelIds: [] }, undefined)).toEqual({ allowed: false, reason: 'model_not_allowed' }); + expect(admitModelExecution(pilot, { ...policy, projectId: 'other' }, undefined)).toEqual({ allowed: false, reason: 'scope_mismatch' }); + expect(admitModelExecution({ ...pilot, maximumCredits: -1 }, policy, undefined)).toEqual({ allowed: false, reason: 'invalid_request' }); + expect(admitModelExecution(request, policy, undefined)).toEqual({ allowed: false, reason: 'reservation_required' }); + }); it('rejects invalid limits and never treats an unknown destination as local', () => { expect(admitModelExecution({ ...request, maxInputTokens: NaN }, policy, hold).allowed).toBe(false); expect(admitModelExecution(request, { ...policy, destination: 'unknown' as never }, hold).allowed).toBe(false); -- 2.54.0