Repository: dottxt-ai/outlines (language: python)

Issue:
Invalid `generate.json` Output with `models.llamacpp`
Related: 
- https://github.com/outlines-dev/outlines/issues/952
- https://github.com/outlines-dev/outlines/issues/985

### Describe the issue as clearly as possible:

`models.llamacpp` used with `generate.json` can result in an output which isn't valid with the pydantic schema.

### Steps/code to reproduce the bug:

```python
# Reproducer from "Robert Roland Roger" on Discord:

import outlines
from pydantic import BaseModel, StringConstraints
from typing import Annotated
import llama_cpp

model = outlines.models.llamacpp(
    "TheBloke/zephyr-7B-beta-GGUF", 
    "zephyr-7b-beta.Q6_K.gguf",
    tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained("HuggingFaceH4/zephyr-7b-beta")
)

class Character(BaseModel):
    name: Annotated[str, StringConstraints(max_length=10)]
    #age: int = Field(..., ge=18, lt=99)
    age: int
    #strength: int = Field(..., ge=1, lt=100)
    strength: int

generator = outlines.generate.json(model, Character)

prompt = """


You always output valid JSON.


Generate a new character in valid json. Use the following fields:
name, age (between 18 and 99), armor beween 1 and 100) and strength.


"""
character = generator(prompt)
```


### Expected result:

An output which pydantic recognizes as valid with the schema.


### Error message:

```shell
Expecting ',' delimiter: line 1 column 28 (char 27) [type=value_error.jsondecode, input_value='{ "name": "Elena", "age": 2', input_type=str]
```


### Outlines/Python version information:

Version information
<details>
```
outlines==0.0.44 
llama-cpp-python==0.2.78 
```
</details>


# Another Reproducer using `models.transformers_multimodal`

```
import outlines

model = outlines.models.transformers_multimodal(
    "llava-hf/llava-v1.6-mistral-7b-hf",
	device="cuda",
	model_kwargs=dict(torch_dtype=torch.bfloat16)
)

from pydantic import BaseModel
from typing import List, Optional

class ImageData(BaseModel):
    caption: str
    tags: List[str]
    location: Optional[str]
    objects: List[str]

image_data_generator = outlines.generate.json(model, ImageData)
image_data_generator(
    "<image> detailed JSON metadata:",
    "https://upload.wikimedia.org/wikipedia/commons/e/ea/FCAB_EMD_GT22CU-3_San_Pedro_-_Ascotan.jpg"
)
```


Discussion / hints:

Fix patch:
diff --git a/outlines/models/llamacpp.py b/outlines/models/llamacpp.py
index a982b080c..aa260a457 100644
--- a/outlines/models/llamacpp.py
+++ b/outlines/models/llamacpp.py
@@ -166,7 +166,9 @@ def prepare_generation_parameters(
 
         # Somehow `llama-cpp-python` generates `max_tokens + 1`  tokens
         if "max_tokens" not in llama_cpp_params:
-            if max_tokens is not None:
+            if max_tokens is None:
+                llama_cpp_params["max_tokens"] = -1  # indicates unlimited tokens
+            else:
                 llama_cpp_params["max_tokens"] = max_tokens - 1
         else:
             llama_cpp_params["max_tokens"] = llama_cpp_params["max_tokens"] - 1


Test patch:
diff --git a/tests/generate/test_integration_llamacpp.py b/tests/generate/test_integration_llamacpp.py
index fcd2bfda9..452d22c36 100644
--- a/tests/generate/test_integration_llamacpp.py
+++ b/tests/generate/test_integration_llamacpp.py
@@ -356,3 +356,22 @@ def test_tokenizer_vocabulary_decode_sanity():
         ]
     )
     assert decoded_nl_token == vocab_nl_token
+
+
+def test_no_length_constraint_when_unset():
+    """Assert that models.llamacpp doesn't have an implicit max_tokens preventing full sequence generation"""
+    import llama_cpp
+
+    model = models.llamacpp(
+        repo_id="M4-ai/TinyMistral-248M-v2-Instruct-GGUF",
+        filename="TinyMistral-248M-v2-Instruct.Q4_K_M.gguf",
+        tokenizer=llama_cpp.llama_tokenizer.LlamaHFTokenizer.from_pretrained(
+            "Locutusque/TinyMistral-248M-Instruct"
+        ),
+    )
+
+    long_pattern = "abcdefg" * 10
+    generator = generate.regex(model, long_pattern)
+
+    output = generator("a")
+    assert re.match(long_pattern, output)