My first OCI Generative AI Python experiments reinforced something I see in many cloud SDKs: the model call itself is simple; the real work is understanding the request hierarchy, serving mode, model identifiers, message representation, and nested response types.

One detail matters immediately: the inference API lives in oci.generative_ai_inference. It is not a separate oci.generative_ai_chat package.

1. Load OCI Configuration

import oci

config = oci.config.from_file(
    file_location="~/.oci/config",
    profile_name="DEFAULT",
)

oci.config.validate_config(config)

The profile typically contains tenancy, user, fingerprint, region, and private-key location. I keep compartment and model identifiers outside source code:

import os

COMPARTMENT_ID = os.environ["OCI_COMPARTMENT_ID"]
MODEL_ID = os.environ["OCI_GENAI_MODEL_ID"]

2. Create the Inference Client

from oci.generative_ai_inference import (
    GenerativeAiInferenceClient
)

client = GenerativeAiInferenceClient(
    config=config
)

This client is for runtime inference operations such as chat and embeddings. I keep OCI management APIs separate from application inference code.

3. Choose a Serving Mode

from oci.generative_ai_inference.models import (
    OnDemandServingMode
)

serving_mode = OnDemandServingMode(
    model_id=MODEL_ID
)

The explicit serving-mode object makes it clear whether the application is using an on-demand model or a dedicated endpoint.

4. Build a Generic Chat Message

from oci.generative_ai_inference.models import (
    UserMessage,
    TextContent,
)

message = UserMessage(
    content=[
        TextContent(
            text=(
                "Explain why an Oracle execution plan "
                "might change after statistics are gathered."
            )
        )
    ]
)

5. Create GenericChatRequest

from oci.generative_ai_inference.models import (
    GenericChatRequest
)

chat_request = GenericChatRequest(
    messages=[message],
    temperature=0.2,
    max_tokens=1000,
)

For database and infrastructure questions I prefer a lower temperature because consistency matters more than creative wording.

6. Wrap the Request in ChatDetails

from oci.generative_ai_inference.models import ChatDetails

details = ChatDetails(
    compartment_id=COMPARTMENT_ID,
    serving_mode=serving_mode,
    chat_request=chat_request,
)

The object hierarchy is worth understanding:

ChatDetails
 ├── compartment_id
 ├── serving_mode
 └── chat_request
      └── messages
           └── UserMessage
                └── TextContent

7. Call the Model and Parse the Nested Response

response = client.chat(details)

chat_response = response.data.chat_response
choice = chat_response.choices[0]
assistant_message = choice.message

text = assistant_message.content[0].text

print(text)

The result is not a flat string. Once function calling is added, this distinction becomes even more important because the assistant message may contain tool_calls in addition to normal content.

8. Hide OCI-Specific Nesting Behind an Adapter

class OciChatProvider:
    def __init__(self, client, compartment_id, model_id):
        self.client = client
        self.compartment_id = compartment_id
        self.model_id = model_id

    def chat(self, text):
        request = GenericChatRequest(
            messages=[
                UserMessage(
                    content=[TextContent(text=text)]
                )
            ],
            temperature=0.2,
            max_tokens=1200,
        )

        details = ChatDetails(
            compartment_id=self.compartment_id,
            serving_mode=OnDemandServingMode(
                model_id=self.model_id
            ),
            chat_request=request,
        )

        response = self.client.chat(details)

        msg = (
            response.data.chat_response
            .choices[0]
            .message
        )

        return msg.content[0].text

The rest of the program can now use a much simpler interface:

provider = OciChatProvider(
    client,
    COMPARTMENT_ID,
    MODEL_ID,
)

answer = provider.chat(
    "What is the difference between "
    "STARTUP MOUNT and ALTER DATABASE OPEN?"
)

9. Capture Useful OCI Errors

from oci.exceptions import ServiceError

try:
    answer = provider.chat("Explain DBMS_XPLAN.")
except ServiceError as exc:
    print("status:", exc.status)
    print("code:", exc.code)
    print("message:", exc.message)
    print("request-id:", exc.request_id)
    raise

Those fields help separate IAM/authentication issues from region, compartment, model availability, and malformed-request problems.

10. Keep Provider Objects at the Edge

Application
    ↓ plain request
OCI adapter
    ↓ SDK request objects
GenerativeAiInferenceClient
    ↓
OCI Generative AI
    ↓ SDK response objects
OCI adapter
    ↓ plain application response

What I Learned

The important OCI concepts become manageable once they are separated: authentication, GenerativeAiInferenceClient, serving mode, GenericChatRequest, ChatDetails, and response parsing. Encapsulating those details made my later RAG and function-calling experiments much easier to evolve.