> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gloo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Working with Bible Translations, Reliably

> Learn how to use output transforms to replace Scripture references with verse text fetched reliably from your selected Bible API.

## Overview

LLMs can misquote Scripture when they recite it from memory: a word substituted, a phrase dropped, a verse attributed to the wrong reference. This recipe uses the **output transform** pattern to remove that risk from the text that matters most: instead of asking the model to quote Scripture, you ask it to emit a reference placeholder like `{{SCRIPTURE:John 3:16}}`, then your own code deterministically fetches and substitutes the real verse text from a Bible API. Every successfully substituted Scripture placeholder contains the text returned by the selected Bible API.

The same technique also gives you translation control that's independent of theological framing. Gloo's `tradition` parameter shapes theological perspective, and as part of that, it applies the King James Version as its scripture quoting standard. If you want a different translation (BSB, NIV, ESV, NASB, or any licensed text), output transforms let you choose it directly, without depending on `tradition` or on how the model quotes from memory.

> [!NOTE]
>
> The placeholder instruction is a system prompt, not a hard constraint. A model could ignore it and include memorized Scripture directly in its commentary. The output transform verifies substituted quotations; it does not verify every piece of model-generated commentary. See [Strengthen This Pattern](#strengthen-this-pattern-for-higher-assurance-applications) for ways to close that gap further.

By the end of this recipe, you'll have a CLI that streams a response from the Gloo AI [Guarded Responses API](/api-guides/responses.md), detects `{{SCRIPTURE:reference}}` placeholders as they arrive, and substitutes real verse text from the [Free Use Bible API](https://bible.helloao.org), all without waiting for the full response to complete.

## Prerequisites

Before starting, ensure you have:

* A Gloo AI Studio account and an [API key](https://studio.ai.gloo.com/api-keys)
* **Authentication setup**: complete the [Authentication Tutorial](/tutorials/authentication.md) first
* No account needed for the Bible API. [Free Use Bible API](https://bible.helloao.org) is free and keyless
* Python 3.9+, Node.js 18+, PHP 8.1+, Go 1.20+, or Java 17+ with Maven, depending on which language you're using

> **[Working Code Sample](https://github.com/GlooDeveloper/gloo-ai-docs-cookbook/tree/main/bible-translation)**
>
> Follow along with complete working examples in all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java).
>
> Setup and testing instructions are provided later.

## Why Output Transforms?

Quoting a passage involves two separate decisions: *which* passage is relevant, and *what* that passage says. Output transform splits them. The model makes the first decision, which stays probabilistic, and emits a reference. Your code makes the second by fetching the text from a Bible API and inserting it after the model has finished. No model runs after the verse text is retrieved, so nothing downstream can reword it.

This isn't a Scripture-specific trick. Having a model write abstract placeholders that tools later fill in with concrete values is an established pattern in LLM research, used to decouple a model's general reasoning from the domain-specific values that tools supply.<sup>[1](#sources)</sup>

For Scripture quotation specifically, a published evaluation of several quotation methods found output transform the most reliable of those tested.<sup>[2](#sources)</sup> That's an observed result from one evaluation, not a universal guarantee about every model or every prompt; treat it as evidence for a design choice, not a proof.

Two alternatives are worth naming:

* **Tool calls**, where the model calls a `get_verse` function and receives the text back in a second round trip before writing its response. The [comparison below](#output-transforms-vs-tool-calls) covers the tradeoffs in detail.
* **RAG (retrieval-augmented generation)**, where verse text is retrieved and stuffed into the prompt context ahead of generation. This helps the model *reference* the right passage, but like tool calls, the model is still the one writing out the quoted text, and nothing enforces that what it writes matches what was retrieved.

Output transform sidesteps both problems for the specific case of Scripture quotation: when the model follows the placeholder instruction, it doesn't see or write the verse text at all. It only writes a reference, and your code does the rest. That's a property of the design, not an enforced guarantee; see the note in the Overview for what happens when a model doesn't comply.

### Output Transforms vs Tool Calls

Here are some things to consider when considering output transform compared to tool calling.

| Aspect | Output Transform | Tool Calls |
| - | - | - |
| **Model writes after the verse is fetched?** | No. Code inserts the fetched text. | Yes. The model writes the final answer, verse included. |
| **Failure mode** | Unreplaced placeholder, or direct model quotation if the prompt is ignored | Incorrect or altered Scripture text |
| **API round trips** | 1 (plus Bible API fetches) | 2+ per verse (multi-round-trip) |
| **Implementation complexity** | Lower (regex + substitution) | Higher (SSE fragment accumulation, conversation history) |
| **Model sees verse text?** | No. It writes commentary from training knowledge. | Yes. Verse text is fed back via the tool result. |
| **Best for** | Scripture quotation accuracy | When the model must reason about verse text before commenting |

The key row is the first one: every generative step placed after retrieval is another chance for the quoted text to change. That matches the evaluation cited above, where output transform outperformed tool calls for exact quotation.

If your use case genuinely needs the model to reason about verse content before responding (summarizing a passage, comparing two verses, answering a question that depends on what a verse actually says), tool calls are the better fit, even with their added complexity. The [Completions with Tool Use tutorial](/tutorials/completions-tool-use.md) covers that pattern in full.

## Step 1: Define the System Prompt

The system prompt is the only thing standing between the model and quoting from memory. It tells the model to emit a placeholder instead of text, and gives it the exact format to use.

```python Python theme={null}
SYSTEM_PROMPT = (
    "Never quote Bible verses directly from memory. When you want to reference "
    "a passage of Scripture, use the placeholder format {{SCRIPTURE:reference}} "
    "(e.g., {{SCRIPTURE:John 3:16}}, {{SCRIPTURE:Romans 8:28-30}}). The system "
    "will replace these placeholders with the actual verse text from the "
    "specified Bible translation. You may still discuss, explain, and comment "
    "on Scripture, but the actual verse text must always come through the "
    "placeholder mechanism. Place each placeholder where the full verse can "
    "stand on its own, such as after a colon or on its own line. Don't use a "
    "placeholder as part of a sentence or inside parentheses."
)
```

```javascript JavaScript theme={null}
const SYSTEM_PROMPT =
  "Never quote Bible verses directly from memory. When you want to reference " +
  "a passage of Scripture, use the placeholder format {{SCRIPTURE:reference}} " +
  "(e.g., {{SCRIPTURE:John 3:16}}, {{SCRIPTURE:Romans 8:28-30}}). The system " +
  "will replace these placeholders with the actual verse text from the " +
  "specified Bible translation. You may still discuss, explain, and comment " +
  "on Scripture, but the actual verse text must always come through the " +
  "placeholder mechanism. Place each placeholder where the full verse can " +
  "stand on its own, such as after a colon or on its own line. Don't use a " +
  "placeholder as part of a sentence or inside parentheses.";
```

```typescript TypeScript theme={null}
const SYSTEM_PROMPT: string =
  "Never quote Bible verses directly from memory. When you want to reference " +
  "a passage of Scripture, use the placeholder format {{SCRIPTURE:reference}} " +
  "(e.g., {{SCRIPTURE:John 3:16}}, {{SCRIPTURE:Romans 8:28-30}}). The system " +
  "will replace these placeholders with the actual verse text from the " +
  "specified Bible translation. You may still discuss, explain, and comment " +
  "on Scripture, but the actual verse text must always come through the " +
  "placeholder mechanism. Place each placeholder where the full verse can " +
  "stand on its own, such as after a colon or on its own line. Don't use a " +
  "placeholder as part of a sentence or inside parentheses.";
```

```php PHP theme={null}
const SYSTEM_PROMPT =
    'Never quote Bible verses directly from memory. When you want to reference '
    . 'a passage of Scripture, use the placeholder format {{SCRIPTURE:reference}} '
    . '(e.g., {{SCRIPTURE:John 3:16}}, {{SCRIPTURE:Romans 8:28-30}}). The system '
    . 'will replace these placeholders with the actual verse text from the '
    . 'specified Bible translation. You may still discuss, explain, and comment '
    . 'on Scripture, but the actual verse text must always come through the '
    . 'placeholder mechanism. Place each placeholder where the full verse can '
    . 'stand on its own, such as after a colon or on its own line. Don\'t use a '
    . 'placeholder as part of a sentence or inside parentheses.';
```

```go Go theme={null}
const systemPrompt = "Never quote Bible verses directly from memory. When you want to reference a passage of Scripture, use the placeholder format {{SCRIPTURE:reference}} (e.g., {{SCRIPTURE:John 3:16}}, {{SCRIPTURE:Romans 8:28-30}}). The system will replace these placeholders with the actual verse text from the specified Bible translation. You may still discuss, explain, and comment on Scripture, but the actual verse text must always come through the placeholder mechanism. Place each placeholder where the full verse can stand on its own, such as after a colon or on its own line. Don't use a placeholder as part of a sentence or inside parentheses."
```

```java Java theme={null}
public static final String SYSTEM_PROMPT =
    "Never quote Bible verses directly from memory. When you want to reference "
    + "a passage of Scripture, use the placeholder format {{SCRIPTURE:reference}} "
    + "(e.g., {{SCRIPTURE:John 3:16}}, {{SCRIPTURE:Romans 8:28-30}}). The system "
    + "will replace these placeholders with the actual verse text from the "
    + "specified Bible translation. You may still discuss, explain, and comment "
    + "on Scripture, but the actual verse text must always come through the "
    + "placeholder mechanism. Place each placeholder where the full verse can "
    + "stand on its own, such as after a colon or on its own line. Don't use a "
    + "placeholder as part of a sentence or inside parentheses.";
```

### Key Points

* This is the *only* instruction shaping the model's behavior toward Scripture: no schema, no tool definition. That simplicity is part of the appeal of output transform.
* The prompt explicitly permits commentary and explanation; it only restricts direct quotation.
* The last two sentences control *where* placeholders go. The verse is substituted as a full sentence with its citation, so a placeholder used as part of a sentence (`{{SCRIPTURE:John 3:16}} shows...`) or inside parentheses produces awkward output. Asking for the verse to stand on its own avoids that.
* In Step 4, this prompt is sent as the Responses `instructions` field.
* Because this is a system prompt and not an enforced constraint, a non-compliant response is possible. The buffer in Step 5 only substitutes what it recognizes as a placeholder. It can't detect a quotation the model wrote out directly.

## Step 2: Write the Bible API Fetch Function

This recipe uses the [Free Use Bible API](https://bible.helloao.org) which is chapter-based, not verse-based. You fetch an entire chapter as JSON and pull out the verse(s) you need. The fetch function has three jobs: parse a human-readable reference like `"Romans 8:28-30"` into a book ID, chapter, and verse list; call the API; and extract the matching verse text from the chapter's structured content.

Book names arrive from model output, so the lookup has to tolerate variation: `"1 John"`, `"I John"`, `"1John"` should all resolve. The full mapping covers all 66 Protestant canon books plus common variants (abbreviated below for readability; see the [cookbook](https://github.com/GlooDeveloper/gloo-ai-docs-cookbook/tree/main/bible-translation) for the complete table).

```python Python theme={null}
import re
import requests

BIBLE_API_URL_TEMPLATE = "https://bible.helloao.org/api/{translation}/{book}/{chapter}.json"

# Full mapping covers all 66 books plus variants (1 John, I John, 1John, ...).
BOOK_IDS = {
    "Genesis": "GEN", "Exodus": "EXO", "Psalm": "PSA", "Psalms": "PSA",
    "Isaiah": "ISA", "Matthew": "MAT", "Mark": "MRK", "Luke": "LUK",
    "John": "JHN", "Romans": "ROM", "1 John": "1JN", "I John": "1JN",
    "1John": "1JN", "Revelation": "REV",
    # ... remaining 66 books and variants
}
CASEFOLDED_BOOK_IDS = {name.casefold(): book_id for name, book_id in BOOK_IDS.items()}

def parse_reference(reference: str):
    normalized = " ".join(reference.strip().split())
    match = re.match(
        r"^(\d?\s*(?:[A-Za-z]+\s+){0,3}[A-Za-z]+)\s+(\d+)(?::(\d+)(?:\s*-\s*(\d+))?)?$",
        normalized,
    )
    if not match:
        raise ValueError(f"Could not parse reference: {reference!r}")

    book_part = " ".join(match.group(1).split())
    chapter = int(match.group(2))
    book_id = CASEFOLDED_BOOK_IDS.get(book_part.casefold())
    if book_id is None:
        book_id = CASEFOLDED_BOOK_IDS.get(book_part.replace(" ", "").casefold())
    if book_id is None:
        raise ValueError(f"Unknown book name: {book_part}")

    if not match.group(3):
        return book_id, chapter, []  # whole chapter
    start = int(match.group(3))
    end = int(match.group(4)) if match.group(4) else start
    return book_id, chapter, list(range(start, end + 1))

def fetch_bible_verse(reference: str, translation: str = "BSB") -> str:
    book_id, chapter, verses = parse_reference(reference)
    url = BIBLE_API_URL_TEMPLATE.format(translation=translation, book=book_id, chapter=chapter)
    response = requests.get(url, timeout=30)
    response.raise_for_status()
    content = response.json().get("chapter", {}).get("content", [])

    def extract_text(fragments):
        parts = []
        for f in fragments:
            if isinstance(f, str):
                parts.append(f)
            elif isinstance(f, dict) and isinstance(f.get("text"), str):
                parts.append(f["text"])
            elif isinstance(f, list):
                parts.append(extract_text(f))
        return " ".join(parts)

    verse_map = {
        item["number"]: extract_text(item.get("content", [])).strip()
        for item in content
        if item.get("type") == "verse" and "number" in item
    }
    if not verses:
        verses = sorted(verse_map.keys())

    missing = [v for v in verses if v not in verse_map]
    if missing:
        raise ValueError(f"Verse(s) not found in {reference}: {missing}")

    selected = " ".join(verse_map[v] for v in verses)
    return f"{selected} ({reference}, {translation})"
```

```javascript JavaScript theme={null}
const BIBLE_API_URL_TEMPLATE = "https://bible.helloao.org/api/{translation}/{book}/{chapter}.json";

// Full mapping covers all 66 books plus variants (1 John, I John, 1John, ...).
const CASEFOLDED_BOOK_IDS = Object.fromEntries(
  Object.entries({
    Genesis: "GEN", Exodus: "EXO", Psalm: "PSA", Psalms: "PSA",
    Isaiah: "ISA", Matthew: "MAT", Mark: "MRK", Luke: "LUK",
    John: "JHN", Romans: "ROM", "1 John": "1JN", "I John": "1JN",
    "1John": "1JN", Revelation: "REV",
    // ... remaining 66 books and variants
  }).map(([name, id]) => [name.toLowerCase(), id])
);

export function parseReference(reference) {
  const normalized = reference.trim().replace(/\s+/g, " ");
  const match = normalized.match(
    /^(\d?\s*(?:[A-Za-z]+\s+){0,3}[A-Za-z]+)\s+(\d+)(?::(\d+)(?:\s*-\s*(\d+))?)?$/
  );
  if (!match) throw new Error(`Could not parse reference: ${reference}`);

  const bookPart = match[1].trim().replace(/\s+/g, " ");
  const chapter = parseInt(match[2], 10);
  let bookId = CASEFOLDED_BOOK_IDS[bookPart.toLowerCase()];
  if (!bookId) bookId = CASEFOLDED_BOOK_IDS[bookPart.replace(/\s+/g, "").toLowerCase()];
  if (!bookId) throw new Error(`Unknown book name: ${bookPart}`);

  if (!match[3]) return { bookId, chapter, verses: [] };
  const start = parseInt(match[3], 10);
  const end = match[4] ? parseInt(match[4], 10) : start;
  return { bookId, chapter, verses: Array.from({ length: end - start + 1 }, (_, i) => start + i) };
}

function extractText(content) {
  const texts = [];
  for (const fragment of content) {
    if (typeof fragment === "string") texts.push(fragment);
    else if (Array.isArray(fragment)) texts.push(extractText(fragment));
    else if (fragment && typeof fragment.text === "string") texts.push(fragment.text);
  }
  return texts.join(" ");
}

export async function fetchBibleVerse(reference, translation = "BSB") {
  const { bookId, chapter, verses } = parseReference(reference);
  const url = BIBLE_API_URL_TEMPLATE.replace("{translation}", translation)
    .replace("{book}", bookId)
    .replace("{chapter}", String(chapter));

  const response = await fetch(url);
  if (!response.ok) throw new Error(`Bible API request failed: ${response.status}`);
  const data = await response.json();

  const verseMap = {};
  for (const item of data?.chapter?.content ?? []) {
    if (item?.type === "verse" && "number" in item) {
      verseMap[item.number] = extractText(item.content ?? []).trim();
    }
  }
  const selectedVerses = verses.length
    ? verses
    : Object.keys(verseMap).map(Number).sort((a, b) => a - b);

  const missing = selectedVerses.filter((v) => !Object.hasOwn(verseMap, v));
  if (missing.length) throw new Error(`Verse(s) not found in ${reference}: ${missing.join(", ")}`);

  const selected = selectedVerses.map((v) => verseMap[v]).join(" ");
  return `${selected} (${reference}, ${translation})`;
}
```

```typescript TypeScript theme={null}
const BIBLE_API_URL_TEMPLATE = "https://bible.helloao.org/api/{translation}/{book}/{chapter}.json";

// Full mapping covers all 66 books plus variants (1 John, I John, 1John, ...).
const BOOK_IDS: Record<string, string> = {
  Genesis: "GEN", Exodus: "EXO", Psalm: "PSA", Psalms: "PSA",
  Isaiah: "ISA", Matthew: "MAT", Mark: "MRK", Luke: "LUK",
  John: "JHN", Romans: "ROM", "1 John": "1JN", "I John": "1JN",
  "1John": "1JN", Revelation: "REV",
  // ... remaining 66 books and variants
};
const CASEFOLDED_BOOK_IDS: Record<string, string> = Object.fromEntries(
  Object.entries(BOOK_IDS).map(([name, id]) => [name.toLowerCase(), id]),
);

function parseReference(reference: string): { bookId: string; chapter: number; verses: number[] } {
  const normalized = reference.trim().split(/\s+/).join(" ");
  const match = normalized.match(/^(\d?\s*(?:[A-Za-z]+\s+){0,3}[A-Za-z]+)\s+(\d+)(?::(\d+)(?:\s*-\s*(\d+))?)?$/);
  if (!match) throw new Error(`Could not parse reference: ${reference}`);

  const bookPart = match[1].split(/\s+/).join(" ");
  const chapter = Number.parseInt(match[2], 10);
  let bookId = CASEFOLDED_BOOK_IDS[bookPart.toLowerCase()];
  if (bookId === undefined) bookId = CASEFOLDED_BOOK_IDS[bookPart.replace(/\s+/g, "").toLowerCase()];
  if (bookId === undefined) throw new Error(`Unknown book name: ${bookPart}`);

  if (match[3] === undefined) return { bookId, chapter, verses: [] };
  const start = Number.parseInt(match[3], 10);
  const end = match[4] !== undefined ? Number.parseInt(match[4], 10) : start;
  return { bookId, chapter, verses: Array.from({ length: end - start + 1 }, (_, i) => start + i) };
}

function extractText(content: unknown): string {
  const parts: string[] = [];
  const walk = (fragment: unknown): void => {
    if (typeof fragment === "string") parts.push(fragment);
    else if (Array.isArray(fragment)) fragment.forEach(walk);
    else if (fragment && typeof fragment === "object") {
      const obj = fragment as { text?: unknown; content?: unknown[] };
      if (typeof obj.text === "string") parts.push(obj.text);
      if (Array.isArray(obj.content)) obj.content.forEach(walk);
    }
  };
  walk(content);
  return parts.join(" ");
}

export async function fetchBibleVerse(reference: string, translation = "BSB"): Promise<string> {
  const { bookId, chapter, verses } = parseReference(reference);
  const url = BIBLE_API_URL_TEMPLATE.replace("{translation}", translation)
    .replace("{book}", bookId)
    .replace("{chapter}", String(chapter));

  const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
  if (!response.ok) throw new Error(`Bible API HTTP error: ${response.status}`);
  const data = await response.json();

  const verseMap = new Map<number, string>();
  for (const item of data.chapter?.content ?? []) {
    if (item.type === "verse" && typeof item.number === "number") {
      verseMap.set(item.number, extractText(item.content ?? []).trim());
    }
  }
  const selectedVerses = verses.length ? verses : [...verseMap.keys()].sort((a, b) => a - b);
  const missing = selectedVerses.filter((v) => !verseMap.has(v));
  if (missing.length > 0) throw new Error(`Verse(s) not found in ${reference}: ${missing.join(", ")}`);

  const selected = selectedVerses.map((v) => verseMap.get(v)!).join(" ");
  return `${selected} (${reference}, ${translation})`;
}
```

```php PHP theme={null}
class BibleApi
{
    public const URL_TEMPLATE = 'https://bible.helloao.org/api/{translation}/{book}/{chapter}.json';

    // Full mapping covers all 66 books plus variants (1 John, I John, 1John, ...).
    private const BOOK_IDS = [
        'Genesis' => 'GEN', 'Exodus' => 'EXO', 'Psalm' => 'PSA', 'Psalms' => 'PSA',
        'Isaiah' => 'ISA', 'Matthew' => 'MAT', 'Mark' => 'MRK', 'Luke' => 'LUK',
        'John' => 'JHN', 'Romans' => 'ROM', '1 John' => '1JN', 'I John' => '1JN',
        '1John' => '1JN', 'Revelation' => 'REV',
        // ... remaining 66 books and variants
    ];

    public static function parseReference(string $reference): array
    {
        $normalized = implode(' ', explode(' ', trim($reference)));
        if (!preg_match(
            '/^(\d?\s*(?:[A-Za-z]+\s+){0,3}[A-Za-z]+)\s+(\d+)(?::(\d+)(?:\s*-\s*(\d+))?)?$/',
            $normalized,
            $m
        )) {
            throw new \InvalidArgumentException("Could not parse reference: {$reference}");
        }

        $bookPart = trim($m[1]);
        $chapter = (int) $m[2];
        $bookId = self::BOOK_IDS[$bookPart] ?? self::BOOK_IDS[str_replace(' ', '', $bookPart)] ?? null;
        if ($bookId === null) {
            throw new \InvalidArgumentException("Unknown book name: {$bookPart}");
        }

        if (!isset($m[3]) || $m[3] === '') {
            return [$bookId, $chapter, []];
        }
        $start = (int) $m[3];
        $end = isset($m[4]) && $m[4] !== '' ? (int) $m[4] : $start;
        return [$bookId, $chapter, range($start, $end)];
    }

    public static function fetchVerseText(string $reference, string $translation = 'BSB'): string
    {
        [$bookId, $chapter, $verses] = self::parseReference($reference);
        $url = str_replace(
            ['{translation}', '{book}', '{chapter}'],
            [$translation, $bookId, (string) $chapter],
            self::URL_TEMPLATE
        );

        $ch = curl_init($url);
        curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30]);
        $body = curl_exec($ch);
        curl_close($ch);
        $data = json_decode($body, true);
        $chapterContent = $data['chapter']['content'] ?? [];

        $verseMap = [];
        foreach ($chapterContent as $item) {
            if (($item['type'] ?? null) === 'verse' && isset($item['number'])) {
                $verseMap[(int) $item['number']] = self::extractText($item['content'] ?? []);
            }
        }
        if (empty($verses)) {
            $verses = array_keys($verseMap);
            sort($verses);
        }

        $selected = array_map(fn($v) => $verseMap[$v], $verses);
        return implode(' ', $selected) . " ({$reference}, {$translation})";
    }

    private static function extractText($content): string
    {
        if (is_string($content)) return $content;
        if (!is_array($content)) return '';
        if (!array_is_list($content)) {
            return $content['text'] ?? self::extractText($content['content'] ?? '');
        }
        return trim(implode(' ', array_map([self::class, 'extractText'], $content)));
    }
}
```

```go Go theme={null}
const bibleAPIURLTemplate = "https://bible.helloao.org/api/%s/%s/%d.json"

// Full mapping covers all 66 books plus variants (1 John, I John, 1John, ...).
var BOOK_IDS = map[string]string{
	"Genesis": "GEN", "Exodus": "EXO", "Psalm": "PSA", "Psalms": "PSA",
	"Isaiah": "ISA", "Matthew": "MAT", "Mark": "MRK", "Luke": "LUK",
	"John": "JHN", "Romans": "ROM", "1 John": "1JN", "I John": "1JN",
	"1John": "1JN", "Revelation": "REV",
	// ... remaining 66 books and variants
}

var casefoldedBookIDs map[string]string

func init() {
	casefoldedBookIDs = make(map[string]string, len(BOOK_IDS))
	for name, id := range BOOK_IDS {
		casefoldedBookIDs[strings.ToLower(name)] = id
	}
}

func ParseReference(reference string) (bookID string, chapter int, verses []int, err error) {
	normalized := strings.Join(strings.Fields(strings.TrimSpace(reference)), " ")
	re := regexp.MustCompile(`^(\d?\s*(?:[A-Za-z]+\s+){0,3}[A-Za-z]+)\s+(\d+)(?::(\d+)(?:\s*-\s*(\d+))?)?$`)
	match := re.FindStringSubmatch(normalized)
	if match == nil {
		return "", 0, nil, fmt.Errorf("could not parse reference: %q", reference)
	}
	bookPart := strings.Join(strings.Fields(match[1]), " ")
	fmt.Sscanf(match[2], "%d", &chapter)

	bookID, ok := casefoldedBookIDs[strings.ToLower(bookPart)]
	if !ok {
		bookID, ok = casefoldedBookIDs[strings.ToLower(strings.ReplaceAll(bookPart, " ", ""))]
	}
	if !ok {
		return "", 0, nil, fmt.Errorf("unknown book name: %s", bookPart)
	}
	if match[3] == "" {
		return bookID, chapter, []int{}, nil
	}
	var start, end int
	fmt.Sscanf(match[3], "%d", &start)
	end = start
	if match[4] != "" {
		fmt.Sscanf(match[4], "%d", &end)
	}
	for i := start; i <= end; i++ {
		verses = append(verses, i)
	}
	return bookID, chapter, verses, nil
}

func FetchBibleVerse(reference, translation string, client *http.Client) (string, error) {
	bookID, chapter, verses, err := ParseReference(reference)
	if err != nil {
		return "", err
	}
	url := fmt.Sprintf(bibleAPIURLTemplate, translation, bookID, chapter)
	resp, err := client.Get(url)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	var data struct {
		Chapter struct {
			Content []interface{} `json:"content"`
		} `json:"chapter"`
	}
	json.NewDecoder(resp.Body).Decode(&data)

	verseMap := make(map[int]string)
	for _, item := range data.Chapter.Content {
		if m, ok := item.(map[string]interface{}); ok && m["type"] == "verse" {
			num := int(m["number"].(float64))
			content, _ := m["content"].([]interface{})
			verseMap[num] = strings.Join(strings.Fields(extractText(content)), " ")
		}
	}
	if len(verses) == 0 {
		for v := range verseMap {
			verses = append(verses, v)
		}
		sort.Ints(verses)
	}

	selected := make([]string, 0, len(verses))
	for _, v := range verses {
		selected = append(selected, verseMap[v])
	}
	return fmt.Sprintf("%s (%s, %s)", strings.Join(selected, " "), reference, translation), nil
}

func extractText(content interface{}) string {
	switch v := content.(type) {
	case string:
		return v
	case []interface{}:
		var parts []string
		for _, item := range v {
			parts = append(parts, extractText(item))
		}
		return strings.Join(parts, " ")
	case map[string]interface{}:
		if t, ok := v["text"].(string); ok {
			return t
		}
		if c, ok := v["content"]; ok {
			return extractText(c)
		}
	}
	return ""
}
```

```java Java theme={null}
public final class BibleApi {
    private static final String BIBLE_API_URL_TEMPLATE = "https://bible.helloao.org/api/%s/%s/%d.json";
    private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
    private static final ObjectMapper MAPPER = new ObjectMapper();

    // Full mapping covers all 66 books plus variants (1 John, I John, 1John, ...).
    private static final Map<String, String> BOOK_IDS = Map.of(
        "Genesis", "GEN", "Exodus", "EXO", "Psalm", "PSA", "Psalms", "PSA",
        "Isaiah", "ISA", "Matthew", "MAT", "John", "JHN", "Romans", "ROM"
        // ... remaining 66 books and variants
    );

    public static String fetchBibleVerse(String reference, String translation) {
        ReferenceParser.Reference parsed = ReferenceParser.parse(reference);
        String url = String.format(BIBLE_API_URL_TEMPLATE, translation, parsed.bookId(), parsed.chapter());

        HttpRequest request = HttpRequest.newBuilder(URI.create(url)).GET().build();
        HttpResponse<String> response;
        try {
            response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
        } catch (IOException | InterruptedException exc) {
            throw new RuntimeException("Failed to fetch Bible chapter: " + exc.getMessage(), exc);
        }

        JsonNode chapterContent;
        try {
            chapterContent = MAPPER.readTree(response.body()).path("chapter").path("content");
        } catch (IOException exc) {
            throw new RuntimeException("Malformed Bible API response", exc);
        }

        List<Integer> verses = new ArrayList<>(parsed.verses());
        if (verses.isEmpty()) {
            for (JsonNode item : chapterContent) {
                if (isVerse(item)) verses.add(item.get("number").asInt());
            }
            Collections.sort(verses);
        }

        List<String> selected = new ArrayList<>();
        for (int verseNumber : verses) {
            selected.add(findVerseText(chapterContent, verseNumber));
        }
        return String.join(" ", selected) + " (" + reference + ", " + translation + ")";
    }

    private static boolean isVerse(JsonNode item) {
        return item.isObject() && "verse".equals(item.path("type").asText()) && item.has("number");
    }

    private static String findVerseText(JsonNode chapterContent, int verseNumber) {
        for (JsonNode item : chapterContent) {
            if (isVerse(item) && item.get("number").asInt() == verseNumber) {
                return extractText(item.path("content")).strip();
            }
        }
        return null;
    }

    private static String extractText(JsonNode node) {
        List<String> parts = new ArrayList<>();
        collectText(node, parts);
        return String.join(" ", parts);
    }

    private static void collectText(JsonNode node, List<String> parts) {
        if (node.isTextual()) {
            parts.add(node.asText().strip());
        } else if (node.isArray()) {
            for (JsonNode child : node) collectText(child, parts);
        } else if (node.isObject()) {
            JsonNode text = node.get("text");
            if (text != null && text.isTextual()) {
                parts.add(text.asText().strip());
            } else if (node.get("content") != null && node.get("content").isArray()) {
                collectText(node.get("content"), parts);
            }
        }
    }
}
```

### Key Points

* The API is chapter-based, so a single-verse request still fetches and filters an entire chapter.
* `parseReference` (or its equivalent) handles ranges (`"Romans 8:28-30"`) by returning a list of verse numbers. A bare chapter reference (`"Psalm 23"`) returns an empty list, which the fetch function treats as "return the whole chapter."
* `extractText` walks the API's nested content structure recursively. Verse content can include footnotes and line breaks as nested objects, and this filters down to plain text.
* If a verse isn't found in the response, the function raises rather than silently returning partial or empty text. A missing verse should surface as an error, not corrupted output.

## Step 3: Set Up Authentication

Load `GLOO_API_KEY` from the environment and send it as a bearer token on every request. See the [Authentication Tutorial](/tutorials/authentication.md) for the full setup, including where to find your key in Gloo AI Studio.

```python Python theme={null}
import os
from dotenv import load_dotenv

load_dotenv()

api_key = os.getenv("GLOO_API_KEY")
if not api_key:
    raise ValueError("GLOO_API_KEY environment variable is required")

headers = {
    "Authorization": f"Bearer {api_key}",
    "Content-Type": "application/json",
}
```

```javascript JavaScript theme={null}
import "dotenv/config";

const apiKey = process.env.GLOO_API_KEY;
if (!apiKey) {
  throw new Error("GLOO_API_KEY environment variable is required");
}

const headers = {
  Authorization: `Bearer ${apiKey}`,
  "Content-Type": "application/json",
};
```

```typescript TypeScript theme={null}
import "dotenv/config";

const apiKey: string | undefined = process.env.GLOO_API_KEY;
if (!apiKey) {
  throw new Error("GLOO_API_KEY environment variable is required");
}

const headers = {
  Authorization: `Bearer ${apiKey}`,
  "Content-Type": "application/json",
};
```

```php PHP theme={null}
require_once __DIR__ . '/vendor/autoload.php';

use Dotenv\Dotenv;

Dotenv::createImmutable(__DIR__)->load();

$apiKey = $_ENV['GLOO_API_KEY'] ?? getenv('GLOO_API_KEY') ?: '';
if ($apiKey === '') {
    fwrite(STDERR, "Error: GLOO_API_KEY is not set.\n");
    exit(1);
}

$headers = [
    'Authorization: Bearer ' . $apiKey,
    'Content-Type: application/json',
];
```

```go Go theme={null}
import "github.com/joho/godotenv"

godotenv.Load() // .env is optional; environment variables take precedence

apiKey := os.Getenv("GLOO_API_KEY")
if apiKey == "" {
    log.Fatal("GLOO_API_KEY environment variable is required")
}

req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
```

```java Java theme={null}
import io.github.cdimascio.dotenv.Dotenv;

Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
String apiKey = dotenv.get("GLOO_API_KEY");
if (apiKey == null || apiKey.isBlank()) {
    System.err.println("Error: GLOO_API_KEY is not set.");
    System.exit(1);
}

HttpRequest request = HttpRequest.newBuilder(URI.create(GLOO_API_URL))
    .header("Authorization", "Bearer " + apiKey)
    .header("Content-Type", "application/json")
    // ...
```

## Step 4: Send the Streaming Request

Send the request to the [Guarded Responses API](/api-guides/responses.md) (`POST /ai/v2/guarded/responses`) with these key inputs:

* The system prompt goes in `instructions`.
* The user's prompt goes in `input`.
* Set `stream: true` to return Server-Sent Events.
* Omit `tradition` to start off. See the [Troubleshooting](#troubleshooting) section for why.

Responses takes a pinned `model`. This recipe uses `gloo-openai-gpt-6-luna`, a fast, low-cost model, set once in a `MODEL` constant so it's easy to change. Any generation model from [Supported Models](/api-guides/supported-models.md) works; see [Using a Different Model](#using-a-different-model).

Each streamed event arrives as an `event:` line followed by a `data:` line. Text arrives in `response.output_text.delta` events, `response.completed` marks the end, and the other `response.*` events are lifecycle events you can ignore. Treat any other event, such as `error`, as the end of the stream.

```python Python theme={null}
GLOO_API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses"
MODEL = "gloo-openai-gpt-6-luna"  # any generation model from Supported Models

def build_request_body(prompt: str, tradition: Optional[str]) -> dict:
    body = {
        "model": MODEL,
        "instructions": SYSTEM_PROMPT,
        "input": prompt,
        "stream": True,
    }
    if tradition is not None:
        body["tradition"] = tradition
    return body

def extract_sse_content_lines(response):
    """Yield text deltas from a Guarded Responses SSE stream."""
    event = None
    for line in response.iter_lines():
        if not line:
            continue
        text = line.decode("utf-8")
        if text.startswith("event:"):
            event = text[len("event:"):].strip()
            continue
        if not text.startswith("data:"):
            continue
        data = json.loads(text[len("data:"):].strip())

        if event == "response.output_text.delta":
            if data.get("delta"):
                yield data["delta"]
        elif event == "response.completed":
            break
        elif event is None or not event.startswith("response."):
            raise RuntimeError(f"Stream terminated by event '{event}': {data}")
        event = None

response = requests.post(
    GLOO_API_URL, headers=headers, json=build_request_body(prompt, tradition),
    stream=True, timeout=60,
)
response.raise_for_status()
```

```javascript JavaScript theme={null}
const GLOO_API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses";
const MODEL = "gloo-openai-gpt-6-luna"; // any generation model from Supported Models

function buildRequestBody(prompt, tradition) {
  const body = {
    model: MODEL,
    instructions: SYSTEM_PROMPT,
    input: prompt,
    stream: true,
  };
  if (tradition !== undefined) body.tradition = tradition;
  return body;
}

async function* extractSSEContentLines(response) {
  const decoder = new TextDecoder("utf-8");
  const reader = response.body.getReader();
  let pending = "";
  let event = null;
  while (true) {
    const { done, value } = await reader.read();
    if (done) return;
    pending += decoder.decode(value, { stream: true });
    const lines = pending.split("\n");
    pending = lines.pop();
    for (let raw of lines) {
      if (raw.endsWith("\r")) raw = raw.slice(0, -1);
      if (raw.startsWith("event:")) {
        event = raw.slice("event:".length).trim();
        continue;
      }
      if (!raw.startsWith("data:")) continue;
      const data = JSON.parse(raw.slice("data:".length).trim());
      const current = event;
      event = null;
      if (current === "response.output_text.delta") {
        if (data.delta) yield data.delta;
      } else if (current === "response.completed") {
        return;
      } else if (current === null || !current.startsWith("response.")) {
        throw new Error(`Stream terminated by event '${current}': ${JSON.stringify(data)}`);
      }
    }
  }
}

const response = await fetch(GLOO_API_URL, {
  method: "POST",
  headers,
  body: JSON.stringify(buildRequestBody(prompt, tradition)),
});
```

```typescript TypeScript theme={null}
const GLOO_API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses";
const MODEL = "gloo-openai-gpt-6-luna"; // any generation model from Supported Models

interface RequestBody {
  model: string;
  instructions: string;
  input: string;
  stream: boolean;
  tradition?: string;
}

function buildRequestBody(prompt: string, tradition: string | undefined): RequestBody {
  const body: RequestBody = {
    model: MODEL,
    instructions: SYSTEM_PROMPT,
    input: prompt,
    stream: true,
  };
  if (tradition !== undefined) body.tradition = tradition;
  return body;
}

// `lines` comes from extractRawSSELines(response), which splits the body on newlines.
async function* extractTextDeltas(lines: AsyncIterable<string>): AsyncGenerator<string> {
  let event: string | undefined;
  for await (const line of lines) {
    const normalizedLine = line.endsWith("\r") ? line.slice(0, -1) : line;
    if (normalizedLine.startsWith("event:")) {
      event = normalizedLine.slice("event:".length).trim();
      continue;
    }
    if (!normalizedLine.startsWith("data:")) continue;
    const data = JSON.parse(normalizedLine.slice("data:".length).trim()) as { delta?: string };
    const current = event;
    event = undefined;
    if (current === "response.output_text.delta") {
      if (data.delta) yield data.delta;
    } else if (current === "response.completed") {
      return;
    } else if (current === undefined || !current.startsWith("response.")) {
      throw new Error(`Stream terminated by event '${current}': ${JSON.stringify(data)}`);
    }
  }
}

const response = await fetch(GLOO_API_URL, {
  method: "POST",
  headers,
  body: JSON.stringify(buildRequestBody(prompt, tradition)),
  signal: AbortSignal.timeout(60_000),
});
```

```php PHP theme={null}
const GLOO_API_URL = 'https://platform.ai.gloo.com/ai/v2/guarded/responses';
const MODEL = 'gloo-openai-gpt-6-luna'; // any generation model from Supported Models

function buildRequestBody(string $prompt, ?string $tradition): array
{
    $body = [
        'model' => MODEL,
        'instructions' => SYSTEM_PROMPT,
        'input' => $prompt,
        'stream' => true,
    ];
    if ($tradition !== null) {
        $body['tradition'] = $tradition;
    }
    return $body;
}

class SseStream
{
    private ?string $event = null;
    private bool $done = false;

    /** Return the text delta for one SSE line, if any. */
    public function parseLine(string $line): ?string
    {
        if ($this->done || $line === '') {
            return null;
        }
        if (str_starts_with($line, 'event:')) {
            $this->event = trim(substr($line, strlen('event:')));
            return null;
        }
        if (!str_starts_with($line, 'data:')) {
            return null;
        }
        $payload = trim(substr($line, strlen('data:')));
        $data = json_decode($payload, true);

        $event = $this->event;
        $this->event = null;
        if ($event === 'response.output_text.delta') {
            return $data['delta'] ?? null;
        }
        if ($event === 'response.completed') {
            $this->done = true;
            return null;
        }
        if ($event === null || !str_starts_with($event, 'response.')) {
            throw new RuntimeException("Stream terminated by event '{$event}': {$payload}");
        }
        return null;
    }
}

$ch = curl_init(GLOO_API_URL);
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => $headers,
    CURLOPT_POSTFIELDS => json_encode(buildRequestBody($prompt, $tradition)),
    CURLOPT_TIMEOUT => 60,
    CURLOPT_WRITEFUNCTION => /* splits chunks into lines for SseStream, see Step 5 */,
]);
curl_exec($ch);
```

```go Go theme={null}
const (
	glooAPIURL = "https://platform.ai.gloo.com/ai/v2/guarded/responses"
	model      = "gloo-openai-gpt-6-luna" // any generation model from Supported Models
)

func BuildRequestBody(prompt, tradition string) ([]byte, error) {
	body := map[string]interface{}{
		"model":        model,
		"instructions": systemPrompt,
		"input":        prompt,
		"stream":       true,
	}
	if tradition != "" {
		body["tradition"] = tradition
	}
	return json.Marshal(body)
}

req, _ := http.NewRequest(http.MethodPost, glooAPIURL, bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
resp, err := http.DefaultClient.Do(req)
// StreamWithSubstitution in Step 5 reads the SSE events from resp.Body.
```

```java Java theme={null}
public static final String GLOO_API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses";
public static final String MODEL = "gloo-openai-gpt-6-luna"; // any generation model from Supported Models

static Map<String, Object> buildRequestBody(String prompt, String tradition) {
    Map<String, Object> body = new LinkedHashMap<>();
    body.put("model", MODEL);
    body.put("instructions", SYSTEM_PROMPT);
    body.put("input", prompt);
    body.put("stream", true);
    if (tradition != null) body.put("tradition", tradition);
    return body;
}

public final class SseStream {
    /** Reads SSE lines and passes each text delta to the consumer. */
    public static void streamContentDeltas(InputStream input, Consumer<String> consumer) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
            String line;
            String event = null;
            while ((line = reader.readLine()) != null) {
                if (line.startsWith("event:")) {
                    event = line.substring("event:".length()).strip();
                    continue;
                }
                if (!line.startsWith("data:")) continue;
                String payload = line.substring("data:".length()).strip();
                JsonNode data = MAPPER.readTree(payload);
                String current = event;
                event = null;
                if ("response.output_text.delta".equals(current)) {
                    JsonNode delta = data.path("delta");
                    if (delta.isTextual() && !delta.asText().isEmpty()) consumer.accept(delta.asText());
                } else if ("response.completed".equals(current)) {
                    break;
                } else if (current == null || !current.startsWith("response.")) {
                    throw new IOException("Stream terminated by event '" + current + "': " + payload);
                }
            }
        }
    }
}

HttpRequest request = HttpRequest.newBuilder(URI.create(GLOO_API_URL))
    .header("Authorization", "Bearer " + apiKey)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(buildRequestBody(prompt, tradition))))
    .build();

HttpResponse<InputStream> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofInputStream());
```

### Key Points

* There's no `[DONE]` sentinel and no `choices[0].delta.content` as in Chat Completions. Text comes only from `response.output_text.delta`, and the final `response.completed` event carries token usage, not the text.
* Responses doesn't support `auto_routing` or `model_family`; those are [Completions V2](/api-guides/completions-v2.md) features. Pinning a model keeps behavior predictable for a recipe whose output depends on the model following the placeholder instruction.
* If the API rejects the request (for example, an unknown model ID), the cookbook prints the API's own reason from the error body, such as `Unknown model '...'`.

## Step 5: Buffer and Substitute Placeholders

This is the core of the output transform. SSE chunks can split anywhere, including in the middle of `{{SCRIPTURE:`. A naive scan-and-replace on each chunk would miss placeholders split across chunk boundaries, so the implementation accumulates text in a buffer and only emits it once it's sure it isn't part of a placeholder still arriving.

The algorithm, per text delta:

1. Append the delta to the buffer.
2. While the buffer contains a complete `{{SCRIPTURE:reference}}` match: emit everything before it, fetch the verse, emit the verse text (or leave the placeholder visible if the fetch fails), and drop the consumed text from the buffer.
3. If the substituted verse already ends its sentence and the model wrote a `.` right after the placeholder, drop that period so the output doesn't read `...through Him. (John 3:17, BSB).` The period can arrive in a later delta, so the check carries over until the next character arrives.
4. Check whether the buffer's tail could still become `{{SCRIPTURE:`, including a lone trailing `{`. If so, hold that tail back; otherwise emit the whole remaining buffer and clear it.
5. After the stream ends, emit whatever is left in the buffer. An incomplete placeholder at that point is shown as-is: a visible reference, not corrupted text.

```python Python theme={null}
PLACEHOLDER_START = "{{SCRIPTURE:"
PLACEHOLDER_RE = re.compile(r"\{\{SCRIPTURE:([^}]+)\}\}")
# Substituted text whose verse already ends a sentence, e.g. "...Him. (John 3:17, BSB)"
ENDS_SENTENCE_RE = re.compile(r"[.!?][\"'”’)]*\s*\([^()]*\)$")

def find_incomplete_prefix(buffer: str) -> Optional[int]:
    marker_index = buffer.rfind(PLACEHOLDER_START)
    if marker_index != -1:
        reference_start = marker_index + len(PLACEHOLDER_START)
        if "}}" not in buffer[reference_start:]:
            return marker_index

    max_prefix_length = min(len(buffer), len(PLACEHOLDER_START) - 1)
    for prefix_length in range(max_prefix_length, 0, -1):
        if buffer.endswith(PLACEHOLDER_START[:prefix_length]):
            return len(buffer) - prefix_length
    return None

def stream_with_substitution(response, translation: str) -> None:
    buffer = ""
    drop_period = False  # set after a verse that already ends a sentence

    def skip_redundant_period(text: str) -> str:
        nonlocal drop_period
        if drop_period and text:
            drop_period = False
            if text.startswith("."):
                return text[1:]
        return text

    for delta in extract_sse_content_lines(response):
        buffer = skip_redundant_period(buffer + delta)

        while True:
            match = PLACEHOLDER_RE.search(buffer)
            if not match:
                break
            print(buffer[: match.start()], end="", flush=True)

            reference = match.group(1).strip()
            try:
                verse_text = fetch_bible_verse(reference, translation)
                substituted = True
            except Exception as exc:
                verse_text = f"{{{{SCRIPTURE:{reference}}}}}"  # graceful failure
                substituted = False
                print(f"\n[Bible API error for {reference}: {exc}]\n", end="", flush=True)

            print(verse_text, end="", flush=True)
            drop_period = substituted and bool(ENDS_SENTENCE_RE.search(verse_text))
            buffer = skip_redundant_period(buffer[match.end():])

        prefix_idx = find_incomplete_prefix(buffer)
        if prefix_idx is not None:
            print(buffer[:prefix_idx], end="", flush=True)
            buffer = buffer[prefix_idx:]
        else:
            print(buffer, end="", flush=True)
            buffer = ""

    print(buffer, end="", flush=True)  # flush any trailing incomplete placeholder
```

```javascript JavaScript theme={null}
const PLACEHOLDER_START = "{{SCRIPTURE:";
const PLACEHOLDER_RE = /\{\{SCRIPTURE:([^}]+)\}\}/;
// Substituted text whose verse already ends a sentence, e.g. "...Him. (John 3:17, BSB)"
const ENDS_SENTENCE_RE = /[.!?]["'”’)]*\s*\([^()]*\)$/;

function findIncompletePrefix(buffer) {
  const markerIndex = buffer.lastIndexOf(PLACEHOLDER_START);
  if (markerIndex !== -1) {
    const referenceStart = markerIndex + PLACEHOLDER_START.length;
    if (!buffer.includes("}}", referenceStart)) return markerIndex;
  }
  const maxPrefixLength = Math.min(buffer.length, PLACEHOLDER_START.length - 1);
  for (let len = maxPrefixLength; len >= 1; len--) {
    if (buffer.endsWith(PLACEHOLDER_START.slice(0, len))) return buffer.length - len;
  }
  return null;
}

async function streamWithSubstitution(deltas, fetchBibleVerse, write) {
  let buffer = "";
  let dropPeriod = false; // set after a verse that already ends a sentence

  const skipRedundantPeriod = (text) => {
    if (dropPeriod && text) {
      dropPeriod = false;
      if (text.startsWith(".")) return text.slice(1);
    }
    return text;
  };

  for await (const delta of deltas) {
    buffer = skipRedundantPeriod(buffer + delta);

    while (true) {
      const match = buffer.match(PLACEHOLDER_RE);
      if (!match) break;
      write(buffer.slice(0, match.index));

      const reference = match[1].trim();
      let verseText;
      let substituted;
      try {
        verseText = await fetchBibleVerse(reference);
        substituted = true;
      } catch (err) {
        verseText = `{{SCRIPTURE:${reference}}}`; // graceful failure
        substituted = false;
        process.stderr.write(`\n[Bible API error for ${reference}: ${err.message}]\n`);
      }
      write(verseText);
      dropPeriod = substituted && ENDS_SENTENCE_RE.test(verseText);
      buffer = skipRedundantPeriod(buffer.slice(match.index + match[0].length));
    }

    const prefixIdx = findIncompletePrefix(buffer);
    if (prefixIdx !== null) {
      write(buffer.slice(0, prefixIdx));
      buffer = buffer.slice(prefixIdx);
    } else {
      write(buffer);
      buffer = "";
    }
  }
  write(buffer); // flush any trailing incomplete placeholder
}
```

```typescript TypeScript theme={null}
const PLACEHOLDER_START = "{{SCRIPTURE:";
const PLACEHOLDER_RE = /\{\{SCRIPTURE:([^}]+)\}\}/g;
// Substituted text whose verse already ends a sentence, e.g. "...Him. (John 3:17, BSB)"
const ENDS_SENTENCE_RE = /[.!?]["'”’)]*\s*\([^()]*\)$/;

function findIncompletePrefix(buffer: string): number | undefined {
  const markerIndex = buffer.lastIndexOf(PLACEHOLDER_START);
  if (markerIndex !== -1) {
    const referenceStart = markerIndex + PLACEHOLDER_START.length;
    if (!buffer.slice(referenceStart).includes("}}")) return markerIndex;
  }
  const maxPrefixLength = Math.min(buffer.length, PLACEHOLDER_START.length - 1);
  for (let len = maxPrefixLength; len > 0; len--) {
    if (buffer.endsWith(PLACEHOLDER_START.slice(0, len))) return buffer.length - len;
  }
  return undefined;
}

async function streamWithSubstitution(
  sseLines: AsyncIterable<string>,
  fetchBible: (reference: string, translation: string) => Promise<string>,
  write: (s: string) => void,
  translation: string,
): Promise<void> {
  let buffer = "";
  let dropPeriod = false; // set after a verse that already ends a sentence

  const skipRedundantPeriod = (text: string): string => {
    if (dropPeriod && text) {
      dropPeriod = false;
      if (text.startsWith(".")) return text.slice(1);
    }
    return text;
  };

  for await (const delta of sseLines) {
    buffer = skipRedundantPeriod(buffer + delta);

    while (true) {
      PLACEHOLDER_RE.lastIndex = 0;
      const match = PLACEHOLDER_RE.exec(buffer);
      if (!match) break;
      write(buffer.slice(0, match.index));

      const reference = match[1].trim();
      let verseText: string;
      let substituted: boolean;
      try {
        verseText = await fetchBible(reference, translation);
        substituted = true;
      } catch (error) {
        verseText = `{{SCRIPTURE:${reference}}}`; // graceful failure
        substituted = false;
        write(`\n[Bible API error for ${reference}: ${String(error)}]\n`);
      }
      write(verseText);
      dropPeriod = substituted && ENDS_SENTENCE_RE.test(verseText);
      buffer = skipRedundantPeriod(buffer.slice(match.index + match[0].length));
    }

    const prefixIdx = findIncompletePrefix(buffer);
    if (prefixIdx !== undefined) {
      write(buffer.slice(0, prefixIdx));
      buffer = buffer.slice(prefixIdx);
    } else {
      write(buffer);
      buffer = "";
    }
  }
  write(buffer); // flush any trailing incomplete placeholder
}
```

```php PHP theme={null}
class SubstitutionStream
{
    public const PLACEHOLDER_START = '{{SCRIPTURE:';
    // Substituted text whose verse already ends a sentence, e.g. "...Him. (John 3:17, BSB)"
    public const ENDS_SENTENCE_RE = '/[.!?]["\'”’)]*\s*\([^()]*\)$/u';

    public static function findIncompletePrefix(string $buffer): ?int
    {
        $markerIndex = strrpos($buffer, self::PLACEHOLDER_START);
        if ($markerIndex !== false) {
            $referenceStart = $markerIndex + strlen(self::PLACEHOLDER_START);
            if (strpos($buffer, '}}', $referenceStart) === false) {
                return $markerIndex;
            }
        }
        $maxPrefixLength = min(strlen($buffer), strlen(self::PLACEHOLDER_START) - 1);
        for ($len = $maxPrefixLength; $len > 0; $len--) {
            if (str_ends_with($buffer, substr(self::PLACEHOLDER_START, 0, $len))) {
                return strlen($buffer) - $len;
            }
        }
        return null;
    }

    // $dropPeriod is owned by the caller so it carries across deltas.
    public static function processCompletePlaceholders(
        string $buffer,
        callable $fetchBibleVerse,
        callable $write,
        ?callable $onError,
        string $translation,
        bool &$dropPeriod = false
    ): string {
        while (preg_match('/\{\{SCRIPTURE:([^}]+)\}\}/', $buffer, $matches, PREG_OFFSET_CAPTURE)) {
            $start = $matches[0][1];
            $end = $start + strlen($matches[0][0]);
            $write(substr($buffer, 0, $start));

            $reference = trim($matches[1][0]);
            try {
                $verseText = $fetchBibleVerse($reference, $translation);
                $substituted = true;
            } catch (\Throwable $exc) {
                $verseText = '{{SCRIPTURE:' . $reference . '}}'; // graceful failure
                $substituted = false;
                if ($onError !== null) $onError($reference, $exc);
            }
            $write($verseText);
            $dropPeriod = $substituted && preg_match(self::ENDS_SENTENCE_RE, $verseText) === 1;
            $buffer = self::skipRedundantPeriod(substr($buffer, $end), $dropPeriod);
        }
        return $buffer;
    }

    public static function skipRedundantPeriod(string $text, bool &$dropPeriod): string
    {
        if ($dropPeriod && $text !== '') {
            $dropPeriod = false;
            if (str_starts_with($text, '.')) {
                return substr($text, 1);
            }
        }
        return $text;
    }
}
// For each delta from SseStream, the processor runs:
//   $buffer = SubstitutionStream::skipRedundantPeriod($buffer . $delta, $dropPeriod);
//   $buffer = SubstitutionStream::processCompletePlaceholders($buffer, ..., $dropPeriod);
// then holds back any incomplete prefix, and flushes the rest after the stream ends.
```

```go Go theme={null}
var placeholderRe = regexp.MustCompile(`\{\{SCRIPTURE:([^}]+)\}\}`)

// Substituted text whose verse already ends a sentence, e.g. "...Him. (John 3:17, BSB)"
var endsSentenceRe = regexp.MustCompile(`[.!?]["'”’)]*\s*\([^()]*\)$`)

const placeholderStart = "{{SCRIPTURE:"

func FindIncompletePrefix(buffer string) *int {
	markerIndex := strings.LastIndex(buffer, placeholderStart)
	if markerIndex != -1 {
		referenceStart := markerIndex + len(placeholderStart)
		if !strings.Contains(buffer[referenceStart:], "}}") {
			return &markerIndex
		}
	}
	maxPrefixLength := len(buffer)
	if maxPrefixLength > len(placeholderStart)-1 {
		maxPrefixLength = len(placeholderStart) - 1
	}
	for prefixLength := maxPrefixLength; prefixLength > 0; prefixLength-- {
		if strings.HasSuffix(buffer, placeholderStart[:prefixLength]) {
			idx := len(buffer) - prefixLength
			return &idx
		}
	}
	return nil
}

func StreamWithSubstitution(r io.Reader, translation string, fetchBibleVerse func(string, string) (string, error), write, errWrite func(string)) error {
	buffer := ""
	event := ""
	dropPeriod := false // set after a verse that already ends a sentence
	skipRedundantPeriod := func(text string) string {
		if dropPeriod && text != "" {
			dropPeriod = false
			if strings.HasPrefix(text, ".") {
				return text[1:]
			}
		}
		return text
	}

	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		line := strings.TrimSuffix(scanner.Text(), "\r")
		if strings.HasPrefix(line, "event:") {
			event = strings.TrimSpace(line[len("event:"):])
			continue
		}
		if !strings.HasPrefix(line, "data:") {
			continue
		}
		payload := strings.TrimSpace(line[len("data:"):])
		var data struct {
			Delta string `json:"delta"`
		}
		if err := json.Unmarshal([]byte(payload), &data); err != nil {
			return fmt.Errorf("received malformed JSON in an SSE data event: %w", err)
		}
		current := event
		event = ""
		if current == "response.completed" {
			break
		}
		if current != "response.output_text.delta" {
			if !strings.HasPrefix(current, "response.") {
				return fmt.Errorf("stream terminated by event '%s': %s", current, payload)
			}
			continue // lifecycle event
		}
		buffer = skipRedundantPeriod(buffer + data.Delta)

		for {
			match := placeholderRe.FindStringSubmatchIndex(buffer)
			if match == nil {
				break
			}
			write(buffer[:match[0]])
			reference := strings.TrimSpace(buffer[match[2]:match[3]])
			verseText, err := fetchBibleVerse(reference, translation)
			substituted := err == nil
			if err != nil {
				verseText = fmt.Sprintf("{{SCRIPTURE:%s}}", reference) // graceful failure
				errWrite(fmt.Sprintf("\n[Bible API error for %s: %v]\n", reference, err))
			}
			write(verseText)
			dropPeriod = substituted && endsSentenceRe.MatchString(verseText)
			buffer = skipRedundantPeriod(buffer[match[1]:])
		}

		prefixIdx := FindIncompletePrefix(buffer)
		if prefixIdx != nil {
			write(buffer[:*prefixIdx])
			buffer = buffer[*prefixIdx:]
		} else {
			write(buffer)
			buffer = ""
		}
	}
	write(buffer) // flush any trailing incomplete placeholder
	return scanner.Err()
}
```

```java Java theme={null}
public final class SubstitutionStream {
    public static final String PLACEHOLDER_START = "{{SCRIPTURE:";
    private static final Pattern PLACEHOLDER_RE = Pattern.compile("\\{\\{SCRIPTURE:([^}]+)\\}\\}");
    // Substituted text whose verse already ends a sentence, e.g. "...Him. (John 3:17, BSB)"
    private static final Pattern ENDS_SENTENCE_RE = Pattern.compile("[.!?][\"'”’)]*\\s*\\([^()]*\\)$");

    public static final class Processor {
        private final StringBuilder buffer = new StringBuilder();
        private final Function<String, String> fetchBibleVerse;
        private final Consumer<String> write;
        private boolean dropPeriod = false; // set after a verse that already ends a sentence

        public Processor(Function<String, String> fetchBibleVerse, Consumer<String> write) {
            this.fetchBibleVerse = fetchBibleVerse;
            this.write = write;
        }

        public void onDelta(String delta) {
            buffer.append(delta);
            skipRedundantPeriod();

            while (true) {
                Matcher matcher = PLACEHOLDER_RE.matcher(buffer.toString());
                if (!matcher.find()) break;
                write.accept(buffer.substring(0, matcher.start()));

                String reference = matcher.group(1).strip();
                String verseText;
                boolean substituted;
                try {
                    verseText = fetchBibleVerse.apply(reference);
                    substituted = true;
                } catch (Exception exc) {
                    verseText = "{{SCRIPTURE:" + reference + "}}"; // graceful failure
                    substituted = false;
                }
                write.accept(verseText);
                dropPeriod = substituted && ENDS_SENTENCE_RE.matcher(verseText).find();
                buffer.delete(0, matcher.end());
                skipRedundantPeriod();
            }

            int prefixIdx = findIncompletePrefix(buffer.toString());
            if (prefixIdx >= 0) {
                write.accept(buffer.substring(0, prefixIdx));
                buffer.delete(0, prefixIdx);
            } else {
                write.accept(buffer.toString());
                buffer.setLength(0);
            }
        }

        private void skipRedundantPeriod() {
            if (dropPeriod && buffer.length() > 0) {
                dropPeriod = false;
                if (buffer.charAt(0) == '.') buffer.deleteCharAt(0);
            }
        }

        public void onEnd() {
            write.accept(buffer.toString()); // flush any trailing incomplete placeholder
            buffer.setLength(0);
        }
    }

    static int findIncompletePrefix(String buffer) {
        int markerIndex = buffer.lastIndexOf(PLACEHOLDER_START);
        if (markerIndex != -1) {
            int referenceStart = markerIndex + PLACEHOLDER_START.length();
            if (!buffer.substring(referenceStart).contains("}}")) return markerIndex;
        }
        int maxPrefixLength = Math.min(buffer.length(), PLACEHOLDER_START.length() - 1);
        for (int len = maxPrefixLength; len > 0; len--) {
            if (buffer.endsWith(PLACEHOLDER_START.substring(0, len))) return buffer.length() - len;
        }
        return -1;
    }
}
```

### Key Points

* On a failed fetch, the placeholder is left visible (`{{SCRIPTURE:reference}}`) rather than replaced with empty or invented text. A visible failure beats a silent one.
* The `find_incomplete_prefix` (or language-equivalent) check is what makes this work across arbitrary SSE chunk boundaries. Without it, a chunk split as `...{{SCRI` / `PTURE:John 3:16}}...` would stream `{{SCRI` to the terminal and never substitute the placeholder.
* The redundant-period check only drops a single `.`, and only after a successful substitution whose verse already ends in `.`, `!`, or `?`. Commas and other punctuation are kept, and so is the period after a placeholder left visible by a failed fetch.
* Nothing here inspects the model's commentary for direct quotations. It only recognizes and substitutes well-formed placeholders. See [Strengthen This Pattern](#strengthen-this-pattern-for-higher-assurance-applications) for closing that gap.

## Step 6: Run the CLI

```bash Python theme={null}
cd bible-translation/python
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
python main.py "Write a three-paragraph devotional reflection on John 3:16."
```

```bash JavaScript theme={null}
cd bible-translation/javascript
npm install
npm start -- "Write a three-paragraph devotional reflection on John 3:16."
```

```bash TypeScript theme={null}
cd bible-translation/typescript
npm install
npm start -- "Write a three-paragraph devotional reflection on John 3:16."
```

```bash PHP theme={null}
cd bible-translation/php
composer install
php index.php "Write a three-paragraph devotional reflection on John 3:16."
```

```bash Go theme={null}
cd bible-translation/go
go mod tidy
go run . "Write a three-paragraph devotional reflection on John 3:16."
```

```bash Java theme={null}
cd bible-translation/java
mvn compile
mvn exec:java -Dexec.args="Write a three-paragraph devotional reflection on John 3:16."
```

Every language accepts the same flags: a required positional `prompt`, an optional `--translation` (default `BSB`), and an optional `--tradition` (omitted by default; see Troubleshooting).

## Example 1: Single Verse

```bash Python theme={null}
python main.py "Write a three-paragraph devotional reflection on John 3:16."
```

```bash JavaScript theme={null}
npm start -- "Write a three-paragraph devotional reflection on John 3:16."
```

```bash TypeScript theme={null}
npm start -- "Write a three-paragraph devotional reflection on John 3:16."
```

```bash PHP theme={null}
php index.php "Write a three-paragraph devotional reflection on John 3:16."
```

```bash Go theme={null}
go run . "Write a three-paragraph devotional reflection on John 3:16."
```

```bash Java theme={null}
mvn exec:java -Dexec.args="Write a three-paragraph devotional reflection on John 3:16."
```

The model writes three paragraphs of commentary and emits one placeholder; the CLI substitutes it with BSB text as the response streams:

```
John 3:16 begins with God’s love—not as a reward for having everything
figured out, but as the source of hope for a world in need. The verse
reminds us that this love is generous and personal: For God so loved the
world that He gave His one and only Son, that everyone who believes in Him
shall not perish but have eternal life. (John 3:16, BSB)

To trust in Jesus is to receive more than an idea; it is to place our lives
in the care of the One who gives himself for us. That trust can steady us
when we feel uncertain and invite us to live with gratitude, humility, and
compassion toward others.

Today, consider where you most need hope, and where you might reflect God’s
love to someone else. You do not have to manufacture perfect faith or
perfect kindness; you can begin with a simple prayer for help to receive
love and share it.
```

Wording will vary between runs since the model generates fresh commentary each time, but the substituted verse text is fixed: every run against the same translation reproduces the same BSB text for John 3:16.

## Example 2: Model-Selected Verses

```bash Python theme={null}
python main.py "Did Jesus come to condemn the world?"
```

```bash JavaScript theme={null}
npm start -- "Did Jesus come to condemn the world?"
```

```bash TypeScript theme={null}
npm start -- "Did Jesus come to condemn the world?"
```

```bash PHP theme={null}
php index.php "Did Jesus come to condemn the world?"
```

```bash Go theme={null}
go run . "Did Jesus come to condemn the world?"
```

```bash Java theme={null}
mvn exec:java -Dexec.args="Did Jesus come to condemn the world?"
```

This prompt doesn't name a verse. The model has to find one that answers the question on its own, and in this run it chose John 3:17:

```
According to the Gospel of John, Jesus says he came not to condemn the 
world, but so the world might be saved through him:

For God did not send His Son into the world to condemn the world, but to 
save the world through Him. (John 3:17, BSB)

The Gospel also speaks of judgment, connected with how people respond to 
Jesus and the light he brings (And this is the verdict: The Light has 
come into the world, but men loved the darkness rather than the Light 
because their deeds were evil. Everyone who does evil hates the Light, 
and does not come into the Light for fear that his deeds will be exposed. 
But whoever practices the truth comes into the Light, so that it 
may be seen clearly that what he has done has been accomplished in 
God.” (John 3:19-21, BSB)). So the short answer is: **his stated purpose 
was salvation, not condemnation**, though his coming also revealed 
people’s choices.
```

This is the point of the pattern: you don't need to know in advance which verses a prompt will touch. The model selects them, and the buffer in Step 5 substitutes however many placeholders arrive, one after another.

## Customizing the Recipe

### Using a Different Model

The recipe pins `gloo-openai-gpt-6-luna` in a single `MODEL` constant (`model` in Go, `Main.MODEL` in Java). To use a different model, change that constant to any generation model ID from [Supported Models](/api-guides/supported-models.md).

After switching, re-run both examples. How reliably a model follows the placeholder instruction, and where it puts placeholders, varies from model to model, and the recipe's output depends on both.

### Using a Licensed Translation

The [Free Use Bible API](https://bible.helloao.org) only serves public-domain and freely-licensed translations, like BSB and WEB. For a licensed translation (NIV, ESV, NASB, NLT, and others), replace the body of `fetch_bible_verse` (or its per-language equivalent) with a call to a licensed provider's API, keeping the same function signature: accept a reference and translation ID, return formatted verse text. Everything else in this recipe (the system prompt, the buffer, the substitution loop) is provider-agnostic and doesn't need to change.

Concrete options include the [YouVersion Platform](https://platform.youversion.com/) and [API.Bible](https://scripture.api.bible/): sign up for API credentials, then point `fetch_bible_verse` at the provider's API instead of Free Use Bible API. Licensed providers typically return copyright or attribution information with each passage. Take it from the API response and display it alongside the verse, rather than letting the model generate it.

### Adding Theological Framing

If you want evangelical, Catholic, or mainline framing but need to keep translation control, add the framing directly to your system prompt instead of setting `tradition`:

```
Never quote Bible verses directly from memory. When you want to reference
a passage of Scripture, use the placeholder format {{SCRIPTURE:reference}}...

Frame your commentary from an evangelical Protestant perspective, consistent
with historic orthodox Christian teaching.
```

This gives you theological framing in the commentary while keeping the placeholder mechanism, and therefore your chosen translation, intact.

## Strengthen This Pattern for Higher-Assurance Applications

The output transform verifies substituted quotations. It does not verify every piece of model-generated commentary. A model that ignores the system prompt could still include a memorized (and possibly inaccurate) quotation directly in its commentary, where the buffer has no way to detect it. If your application needs a stronger guarantee than "every successfully substituted placeholder is verified," here are four ways to close that gap, roughly in order of effort:

* [Validate Before Displaying, and Fail Closed](#validate-before-displaying-and-fail-closed)
* [Compare Commentary Against the Translation](#compare-commentary-against-the-translation)
* [Use Structured Output with Typed Segments](#use-structured-output-with-typed-segments)
* [Visually Distinguish API-Sourced Quotations](#visually-distinguish-api-sourced-quotations)

None of these are implemented in this recipe. They're documented here as a starting point if your application needs more than what output transform guarantees on its own.

### Validate Before Displaying, and Fail Closed

Buffer the complete response instead of streaming it straight to the user, run a check, and only then render it. Treat the response as invalid (and show an error or retry, rather than displaying it) if a placeholder survives to the end of the stream unsubstituted, or if the commentary contains text that looks like a verbatim quotation. This trades away the live-streaming behavior this recipe demonstrates in exchange for the chance to inspect the full response before anything reaches the user.

Whatever you do on failure, don't fall back to asking the model to quote the verse itself when the Bible API is unavailable. That silently swaps an API-sourced quotation for a memorized one, which is the exact risk this pattern exists to remove.

### Compare Commentary Against the Translation

Search for exact or near-exact matches between the model's commentary and the selected translation's text, and reject responses where one is found. A practical version of this needs:

* Access to the complete selected translation, not just the verses referenced this turn.
* Normalization of punctuation, capitalization, and whitespace before comparing.
* A searchable n-gram index, with a defined threshold for what counts as a quotation (for example, six or more consecutive matching words).
* Fail-closed behavior when a match is found.
* Testing against short, commonly repeated phrases to check for false positives.

Even fully built, this only catches exact or near-exact matches under whatever threshold you choose, not every possible paraphrase.

### Use Structured Output with Typed Segments

Structured output can enforce a response's *shape*; it can't by itself prove commentary contains no memorized Scripture. That distinction matters for what this approach can and can't promise.

Instead of streaming free text with inline placeholders, have the model return a single JSON document with typed segments:

```json theme={null}
{
  "segments": [
    { "kind": "commentary", "value": "This passage emphasizes God's love..." },
    { "kind": "scripture", "value": "John 3:16" }
  ]
}
```

A strict JSON schema (passed via `response_format`, if your target model and endpoint support it) can enforce that every segment has a valid `kind` and `value`. The client then buffers the full response, parses and validates it, rejects unknown fields or malformed references, fetches every `scripture` segment from the Bible API, and renders only the fetched text as a quotation, failing closed if validation or retrieval fails. Check that your pinned model supports it, since structured-output support and behavior vary by provider.

This is a real guarantee, but a narrower one than it might first appear:

> Segments designated as Scripture quotations are deterministically sourced from the selected Bible API; invalid or unfetched references are never rendered as quotations.

It does **not** guarantee that Scripture text can't appear inside a `commentary` segment. `{"kind": "commentary", "value": "For God so loved the world..."}` is structurally valid despite containing a memorized quotation. Closing that gap requires layering the commentary-comparison approach above on top.

It also changes the streaming behavior this recipe demonstrates: because the full JSON must be validated before anything is safe to render, this trades live token-by-token streaming for a single validated response that can only be rendered progressively at *display* time, not *generation* time. That tradeoff, plus the added schema validation and model-compatibility handling, is why this recipe doesn't implement it: the complexity is real, and it doesn't fully close the underlying semantic risk on its own.

### Visually Distinguish API-Sourced Quotations

In a UI, render substituted verse text with a distinct style, such as a citation card or a different background, so users can see at a glance which words came from the Bible API and which came from the model's own commentary. This doesn't prevent a non-compliant response; it makes the distinction legible to the reader when one occurs.

## Working Code Sample

> **[View Complete Code](https://github.com/GlooDeveloper/gloo-ai-docs-cookbook/tree/main/bible-translation)**
>
> Clone or browse the complete working examples for all 6 languages (JavaScript,
> TypeScript, Python, PHP, Go, Java) with setup instructions and environment
> configuration.

> [!TIP]
>
> The code snippets in this tutorial are **simplified and self-contained**. The book-name mapping is abbreviated for readability, and the logic that's split across multiple files in PHP, Go, and Java (`BibleApi`, `SseStream`, `SubstitutionStream`, and similar) is shown inline here. The [cookbook examples](https://github.com/GlooDeveloper/gloo-ai-docs-cookbook/tree/main/bible-translation) include the full 66-book mapping and the production file layout. Both implement the same APIs and patterns.

## Next Steps

Now that you've completed this recipe, you can:

* **[Completions with Tool Use](/tutorials/completions-tool-use.md)**: the alternative pattern for scripture quotation when the model needs to reason about verse text
* **[Using the Guarded Responses API](/tutorials/responses.md)**: more on the Responses request shape, `tradition`, and streaming events
* **[Authentication Tutorial](/tutorials/authentication.md)**: full API key setup and management
* **[Using a Licensed Translation](#using-a-licensed-translation)**: swap in NIV, ESV, or another licensed text via YouVersion or another provider

## Troubleshooting

### The model quotes Scripture directly instead of using a placeholder

**Cause**: The system prompt is a soft instruction, not an enforced constraint. Some prompts or models are more prone to ignoring it, and setting `tradition` adds instructions of its own (see below).

**Solution**: Check your system prompt is being sent on every request (not just the first, if you're maintaining conversation history). If direct quotation persists, add one of the checks from [Strengthen This Pattern](#strengthen-this-pattern-for-higher-assurance-applications), such as a post-response check for verbatim text matching the translation.

### Placeholder appears in the output unsubstituted, e.g. `{{SCRIPTURE:John 3:16}}`

**Cause**: The Bible API fetch failed, usually because of an unknown book name, a bad chapter/verse number, or a network error. This is the intended graceful-failure behavior: the code leaves the placeholder visible rather than guessing.

**Solution**: Check stderr for a `[Bible API error for ...]` message with the underlying cause. If it's an unknown book name, extend the `BOOK_IDS` mapping with the variant the model used.

### The API returns HTTP 400 for an unknown model

**Cause**: The `MODEL` constant names a model ID that doesn't exist or is no longer offered. The CLI prints the API's message, for example `Error: Gloo AI API returned HTTP 400: Unknown model '...'`.

**Solution**: Pick a current generation model ID from [Supported Models](/api-guides/supported-models.md) and update `MODEL`. Models marked deprecated keep working for a time but should be replaced.

### The verse reads awkwardly inside a sentence

**Cause**: The model used a placeholder as part of a sentence or inside parentheses, so the full verse and its citation landed mid-sentence.

**Solution**: Make sure the system prompt includes the placement sentences from Step 1 ("Place each placeholder where the full verse can stand on its own..."). If you've customized the prompt, keep those sentences.

### "Unknown book name" error

**Cause**: The model referenced a book name or abbreviation not present in the lookup table.

**Solution**: Add the missing variant to `BOOK_IDS`. The cookbook's mapping already covers common forms (`"1 John"`, `"I John"`, `"1John"`, `"First John"`), but models can still produce unexpected phrasing.

### Streaming output looks corrupted or a placeholder gets split across lines

**Cause**: Usually a symptom of bypassing the buffer, for example by printing SSE content deltas directly instead of routing them through `stream_with_substitution` (or the language equivalent).

**Solution**: Confirm every content delta flows through the buffering function before being written to output. Direct prints will occasionally split a `{{SCRIPTURE:...}}` marker across two writes.

### The `tradition` parameter may produce KJV-flavored commentary

**Cause**: Setting `tradition` adds Gloo's own values-alignment instructions to the request, including the King James Version as its quoting standard. Placeholder substitution still works: the `{{SCRIPTURE:...}}` reference is emitted and substituted with your chosen translation. But the model's own *commentary* can pick up KJV-style phrasing or paraphrase verse wording in prose. This depends on the model: with an earlier auto-routed setup, commentary sometimes included lines such as `"...that whosoever believeth in him should not perish, but have everlasting life"`, while repeated test runs with `gloo-openai-gpt-6-luna` didn't show it.

**Solution**: Leave `tradition` unset, which is the recipe's default. If you need theological framing, add it directly to your system prompt instead; see [Adding Theological Framing](#adding-theological-framing). If you use `tradition`, treat the commentary (not just the placeholders) as untrusted, and consider the quotation-detection strategies in [Strengthen This Pattern](#strengthen-this-pattern-for-higher-assurance-applications).

## Sources

1. Gao, S., et al. [*Efficient Tool Use with Chain-of-Abstraction Reasoning*](https://arxiv.org/abs/2401.17464). COLING 2025.
2. The Apologist Project and Fide AI. [*Scripture Quotation in Generative AI*](https://apologistproject.org/scripture-quotation-in-generative-ai). The test harness is open source at [apologist-project/llm-scripture-fidelity](https://github.com/apologist-project/llm-scripture-fidelity).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.