private CompletableFuture<Completion.FinishReason> completeWithOffer(
            Prompt prompt, InferenceParameters options, Consumer<Completion> consumer, long offerTimeout) {
        var completionFuture = new CompletableFuture<Completion.FinishReason>();
        
        var promptStr = prompt.asString().stripLeading();
        var promptTokens = model.encode(promptStr);
        
        // Truncate prompt
        if (maxPromptTokens > 0 && promptTokens.length > maxPromptTokens) {
            promptTokens = Arrays.copyOfRange(promptTokens, 0, maxPromptTokens + 1);
            promptStr = model.decode(promptTokens);
            prompt = StringPrompt.from(promptStr);
        }
        
        var numPromptTokens = promptTokens.length;
        var numRequestTokens = numPromptTokens + maxTokens;
        logger.fine(() -> String.format("Prompt tokens: %d, max tokens: %d, request tokens: %d", 
                numPromptTokens, maxTokens, numRequestTokens));

        // Do something when context size is too small for this request
        if (numRequestTokens > contextSizePerRequest) {
            switch (contextOverflowPolicy) {
                case FAIL:
                    var errorMessage = String.format(
                            "Context size per request (%d tokens) is too small " +
                                    "to fit the prompt (%d) and completion (%d) tokens.",
                            contextSizePerRequest, promptTokens.length, maxTokens);
                    completionFuture.completeExceptionally(new LanguageModelException(413, errorMessage));
                    return completionFuture;
                case DISCARD:
                    completionFuture.complete(Completion.FinishReason.discard);
                    return completionFuture;
                case NONE:
                    break;
            }
        }

        var inferenceParams = setInferenceParameters(prompt, options);
        var hasStarted = new AtomicBoolean(false);
        var future = new FutureTask<>(() -> {
            hasStarted.set(true);

            try {
                // Actual generation, stream outputs
                for (var output : model.generate(inferenceParams)) {
                    consumer.accept(Completion.from(output.text, Completion.FinishReason.none));
                }
                completionFuture.complete(Completion.FinishReason.stop);
            } catch (Exception e) {
                var errorMessage = "Error while generating completion in executor thread.";
                completionFuture.completeExceptionally(new LanguageModelException(500, errorMessage, e));
            }
        }, null);
        
        try {
            var accepted = offerTimeout > 0 
                    ? executor.getQueue().offer(future, offerTimeout, TimeUnit.MILLISECONDS) 
                    : executor.getQueue().offer(future);
                
            if (!accepted) {
                String errorMessage = rejectedExecutionErrorMessage(
                        "Rejected completion due to timeout waiting to add the request to the executor queue");
                completionFuture.completeExceptionally(new LanguageModelException(504, errorMessage));
                return completionFuture;
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            String errorMessage = rejectedExecutionErrorMessage(
                    "Rejected completion due to interruption when adding the request to the executor queue");
            completionFuture.completeExceptionally(new LanguageModelException(500, errorMessage));
            return completionFuture;
        }
        
        if (maxQueueWait > 0) {
            scheduler.schedule(
                    () -> {
                        if (!hasStarted.get()) {
                            future.cancel(false);
                            executor.remove(future);
                            String errorMessage = rejectedExecutionErrorMessage(
                                    "Rejected completion due to timeout waiting to start processing the request");
                            completionFuture.completeExceptionally(new LanguageModelException(504, errorMessage));
                        }
                    }, maxQueueWait, TimeUnit.MILLISECONDS
            );
        }
        
        return completionFuture;
    }
Completes the given prompt, mostly asynchronously with or without a blocking wait.
It is used by both `complete()` and `completeAsync()` with different `offerTimeout` values.
When set to 0, there is no blocking wait, the request is either added to the queue or rejected immediately.
When larger than 0 and the queue is full, there will be a blocking wait up to `offerTimeout` milliseconds.
This blocking is used for throttling the incoming requests, propagating the delay up the stack.
model-integration/src/main/java/ai/vespa/llm/clients/LocalLLM.java