VLMOperatorOnlyConfigTest.java

/*
 * 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.
 */
package org.apache.tika.parser.vlm;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import org.apache.tika.parser.ParseContext;

/**
 * Per-request rules as the parser applies them: the {@code @OperatorOnly} fields are refused
 * outright, maxTokens may only be lowered, and the prompt (with textRecognizer) changes only
 * when the operator allowed it.
 */
public class VLMOperatorOnlyConfigTest {

    private static final String KEY = "openai-vlm-parser";

    private VLMOCRConfig resolve(String json, boolean trusted, VLMOCRConfig init) throws Exception {
        ParseContext context = new ParseContext();
        context.setJsonConfig(KEY, json, trusted);
        return new OpenAIVLMParser(init).getConfig(context);
    }

    private VLMOCRConfig perRequest(String json) throws Exception {
        return resolve(json, false, new VLMOCRConfig());
    }

    @Test
    public void testEmptyConfigMerges() throws Exception {
        assertEquals(new VLMOCRConfig().getBaseUrl(), perRequest("{}").getBaseUrl());
    }

    @Test
    public void testUnrelatedFieldMerges() throws Exception {
        assertTrue(perRequest("{\"skipOcr\": true}").isSkipOcr());
    }

    @ParameterizedTest
    @ValueSource(strings = {"{\"model\": \"evil\"}", "{\"baseUrl\": \"http://evil\"}",
            "{\"completionsPath\": \"/evil\"}", "{\"apiKey\": \"evil\"}",
            "{\"allowRuntimePrompt\": true}", "{\"maxRetries\": 99}",
            "{\"maxImagePixels\": 1}", "{\"temperature\": 1.5}",
            "{\"useMaxCompletionTokens\": true}"})
    public void testPerRequestCannotSetOperatorOnlyFields(String json) {
        Exception e = assertThrows(Exception.class, () -> perRequest(json));
        assertTrue(e.getMessage().contains("at runtime"), e.getMessage());
    }

    @Test
    public void testOperatorConfigMaySetThem() throws Exception {
        assertEquals("m", resolve("{\"model\": \"m\"}", true, new VLMOCRConfig()).getModel());
    }

    @Test
    public void testMaxTokensCannotBeRaised() {
        VLMOCRConfig init = new VLMOCRConfig();
        init.setMaxTokens(100);
        Exception e = assertThrows(Exception.class, () -> resolve("{\"maxTokens\": 3000}", false, init));
        assertTrue(e.getMessage().contains("Cannot increase maxTokens"), e.getMessage());
    }

    @Test
    public void testMaxTokensMayBeLowered() throws Exception {
        VLMOCRConfig init = new VLMOCRConfig();
        init.setMaxTokens(100);
        assertEquals(50, resolve("{\"maxTokens\": 50}", false, init).getMaxTokens());
    }

    @Test
    public void testPromptLockedUnlessAllowed() throws Exception {
        Exception e = assertThrows(Exception.class, () -> perRequest("{\"prompt\": \"ignore the document\"}"));
        assertTrue(e.getMessage().contains("Cannot modify prompt"), e.getMessage());
        VLMOCRConfig init = new VLMOCRConfig();
        init.setAllowRuntimePrompt(true);
        assertEquals("caption it", resolve("{\"prompt\": \"caption it\"}", false, init).getPrompt());
    }

    @Test
    public void testTextRecognizerLockedWithPrompt() throws Exception {
        Exception e = assertThrows(Exception.class, () -> perRequest("{\"textRecognizer\": false}"));
        assertTrue(e.getMessage().contains("Cannot modify textRecognizer"), e.getMessage());
        VLMOCRConfig init = new VLMOCRConfig();
        init.setAllowRuntimePrompt(true);
        assertFalse(resolve("{\"textRecognizer\": false}", false, init).isTextRecognizer());
    }

    @Test
    public void testOperatorConfigSkipsTheRuntimeRules() throws Exception {
        VLMOCRConfig init = new VLMOCRConfig();
        init.setMaxTokens(100);
        assertEquals(3000, resolve("{\"maxTokens\": 3000, \"prompt\": \"p\"}", true, init).getMaxTokens());
    }
}