Datalumina

The backend

The LLM service

Connect to the Azure OpenAI deployment with the standard OpenAI SDK and prove the second extractor works.

By the end of this stage both extractors are proven. Next to the Document Intelligence service sits an Azure OpenAI service, and a playground script gets a model response back through your Foundry deployment.

daveebbelaar/invoice-review0:45:45

One difference

The surprise of this stage is how little Azure changes. You use the standard OpenAI Python SDK, the same from openai import OpenAI you would use against OpenAI directly. Dave lands on the point after the first successful call: "there's just literally one difference. It's the endpoint."

Point base_url at your Foundry resource and every request routes through Azure, with the security and data-protection story that made Azure the choice in the first place. Same SDK, same request shape, same pricing.

backend/app/services/azure_openai_service.py
from openai import OpenAI
from openai.types.responses import Response

from app.config import DEPLOYMENT_NAME, Settings, get_settings


class AzureOpenAIService:
    def __init__(self, settings: Settings | None = None) -> None:
        resolved_settings = settings or get_settings()
        self._client = OpenAI(
            base_url=resolved_settings.azure_openai_endpoint,
            api_key=resolved_settings.azure_openai_api_key,
        )
        self._deployment = resolved_settings.azure_openai_deployment or DEPLOYMENT_NAME

    def create_response(self, input: str) -> Response:
        return self._client.responses.create(
            model=self._deployment,
            input=input,
        )

The deployment name is the model name

Against OpenAI directly you pass a model name. Against Azure you pass your deployment name, the one you created in the Foundry stage. This build uses gpt-5.6-terra, declared once in config:

backend/app/config.py
DEPLOYMENT_NAME = "gpt-5.6-terra"

If the call fails with a model-not-found error, the deployment name in .env does not match what exists in your Foundry project. That is the single most common failure at this stage, and it is why the earlier page insisted a fresh Foundry resource has no deployments until you create one.

The endpoint format matters

The AZURE_OPENAI_ENDPOINT value must end in /openai/v1/, as shown in .env.example. With that suffix the standard SDK speaks to Azure as if it were OpenAI. This detail comes back with force in the deployment stage, where a script normalizes the URL before it goes into the container.

Test it from the playground

Same pattern as the extraction service, one throwaway script:

playground/create_openai_response.py
def main() -> None:
    prompt = "What is the capital of France?"
    service = AzureOpenAIService()
    response = service.create_response(prompt)
    print(response.output[0].content[0].text)
cd playground
uv run --project ../backend --locked --no-sync python create_openai_response.py
# The capital of France is Paris.

The one non-obvious part is digging the text out of the Responses API result. The answer lives at response.output[0].content[0].text, and on Azure the full response carries extra metadata such as content-filter results. Dump the whole object once with response.model_dump() to see what you are working with.

Where this leaves the build

This is the first milestone. In Dave's words: "we can take a document, send it to an endpoint and get information back, and we also have an LLM." Two independent services, each proven in isolation from the playground, neither knowing the other exists. Everything from here on is business logic that composes them.

Checkpoint

  • create_openai_response.py returns an answer through your Azure endpoint
  • You can say what the one difference is between calling OpenAI and calling Azure OpenAI
  • You know where the deployment name lives in config and in .env

On this page