Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,14 @@
# OPENAI_API_KEY=<your key>
# ANTHROPIC_API_KEY=<your key>

# Oracle Cloud Infrastructure Generative AI (litellm `oci/`; see docs/oci-generative-ai.md)
# OCI_COMPARTMENT_ID=<compartment ocid> # selects OCI in the quickstart examples
# OCI_REGION=us-chicago-1
# OCI_CLI_PROFILE=DEFAULT # ~/.oci/config profile (needs `uv pip install oci`); or set the four variables below
# OCI_USER=<user ocid>
# OCI_TENANCY=<tenancy ocid>
# OCI_FINGERPRINT=<api key fingerprint>
# OCI_KEY_FILE=~/.oci/oci_api_key.pem

# Prevent litellm from fetching model costs from GitHub (uses bundled local data)
LITELLM_LOCAL_MODEL_COST_MAP=True
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ to follow semantic versioning.

## [Unreleased]

- Add Oracle Cloud Infrastructure Generative AI as a documented provider: the
quickstart selector picks `oci/` models when `OCI_COMPARTMENT_ID` is set, signs
with an `~/.oci/config` profile via `OCI_CLI_PROFILE` or with `OCI_*` API-key
variables, and takes `OCI_MODEL` and `OCI_ENDPOINT_ID` overrides for imported models; new
`docs/oci-generative-ai.md` and `examples/quickstart/16_oci_generative_ai.py`.
- Breaking: custom CodeAct error formatters must implement
`format(error, code=None, *, line_offset=0, max_error=None, tail_chars=None)`.
Reduced legacy signatures are no longer supported.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ llm = get_llm_client("claude-haiku-4-5")
llm = get_llm_client("gpt-5-mini") # OpenAI (after `export OPENAI_API_KEY=...`)
llm = get_llm_client("ollama_chat/qwen3:1.7b", api_base="http://localhost:11434") # Ollama (no key)
llm = get_llm_client("hosted_vllm/Qwen/Qwen3-1.7B", api_base="http://localhost:8000/v1") # vLLM (no key)
llm = get_llm_client("oci/meta.llama-3.3-70b-instruct", oci_region="us-chicago-1", oci_compartment_id="ocid1.compartment...") # OCI Generative AI (after `export OCI_*` credentials)
```

### 2. Your first agent
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ directory. For code you can run immediately, use the
| [Tools and visibility](concepts/tools-and-visibility.md) | How does generated code discover and call capabilities? |
| [Prompts and context](concepts/prompts-and-context.md) | Where should instructions, inputs, and cross-call information live? |
| [Local models](local-models.md) | How do I run NOOA with local models? |
| [OCI Generative AI](oci-generative-ai.md) | How do I run NOOA on Oracle Cloud Infrastructure Generative AI, including imported Nemotron models? |
| [Orchestration](concepts/orchestration.md) | How do I make a workflow deterministic without turning it into one giant prompt? |
| [Multi-agent systems](concepts/multi-agent-systems.md) | When should I use another agent, and what state does it share? |
| [Tracing](concepts/tracing.md) | How do I inspect the complete Python and LLM call tree? |
Expand Down
175 changes: 175 additions & 0 deletions docs/oci-generative-ai.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# OCI Generative AI

NOOA uses LiteLLM model strings through `get_llm_client()`, and LiteLLM routes
`oci/<model>` to [Oracle Cloud Infrastructure (OCI) Generative AI](https://docs.oracle.com/en-us/iaas/Content/generative-ai/home.htm).
Any model in the OCI Generative AI catalog, and any model you import into a
dedicated endpoint, can drive a NOOA agent without changing agent code.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

```python
from nooa.unifiedllm.registry import get_llm_client

llm = get_llm_client(
"oci/meta.llama-3.3-70b-instruct",
oci_region="us-chicago-1",
oci_compartment_id="ocid1.compartment.oc1..example",
)
```

Requests go to `https://inference.generativeai.<region>.oci.oraclecloud.com`.
The region defaults to `us-ashburn-1`; set it to a region where your tenancy is
subscribed to the service. `oci_compartment_id` is required.

## Authentication

LiteLLM signs OCI requests itself. Two options:

**API-key credentials.** Set the values from your OCI user's API key as
parameters or as environment variables, which LiteLLM reads automatically:

```bash
export OCI_REGION=us-chicago-1
export OCI_COMPARTMENT_ID=ocid1.compartment.oc1..example
export OCI_USER=ocid1.user.oc1..example
export OCI_TENANCY=ocid1.tenancy.oc1..example
export OCI_FINGERPRINT=aa:bb:cc:...
export OCI_KEY_FILE=~/.oci/oci_api_key.pem # or OCI_KEY with the PEM contents
```

**An OCI SDK signer.** If you already use the `oci` CLI, reuse a profile from
`~/.oci/config`, including session-token profiles created by
`oci session authenticate`, by passing a signer object:

```python
import os

import oci

from nooa.unifiedllm.registry import get_llm_client

config = oci.config.from_file(profile_name="DEFAULT")
if "security_token_file" in config:
with open(os.path.expanduser(config["security_token_file"])) as f:
token = f.read().strip()
signer = oci.auth.signers.SecurityTokenSigner(
token, oci.signer.load_private_key_from_file(config["key_file"])
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
)
else:
signer = oci.signer.Signer(
tenancy=config["tenancy"],
user=config["user"],
fingerprint=config["fingerprint"],
private_key_file_location=config["key_file"],
pass_phrase=config.get("pass_phrase"),
)

llm = get_llm_client(
"oci/meta.llama-3.3-70b-instruct",
oci_signer=signer,
oci_region=config.get("region", "us-ashburn-1"),
oci_compartment_id=os.environ["OCI_COMPARTMENT_ID"],
)
```

The `oci` SDK is not a NOOA dependency; install it with `uv pip install oci`.
The quickstart selector implements both options behind `OCI_CLI_PROFILE` and the
`OCI_*` variables; see [Quickstart selector](#quickstart-selector).

## Choose a model

Use the catalog model id after `oci/`. The default below is the one exercised
with NOOA's Predict and CodeAct strategies; the others are catalog examples that
LiteLLM routes the same way:

| Model string | Notes |
| --- | --- |
| `oci/meta.llama-3.3-70b-instruct` | Default; tested with NOOA quickstarts 01, 02, 03, and 16 |
| `oci/meta.llama-4-maverick-17b-128e-instruct-fp8` | Multimodal |
| `oci/xai.grok-4` | Reasoning model |
| `oci/google.gemini-2.5-pro` | Reasoning model, multimodal |
| `oci/openai.gpt-oss-120b` | Open-weights MoE |
| `oci/cohere.command-a-03-2025` | Cohere family |

Availability differs by region. List the catalog for your compartment with
`oci generative-ai model-collection list-models --compartment-id <ocid> --region <region>`.

## NVIDIA Nemotron on OCI

The Generative AI catalog does not include Nemotron models by default. Two ways to run them:

**Imported model on a dedicated endpoint.** OCI Generative AI can
[import open-weights models](https://docs.oracle.com/en-us/iaas/Content/generative-ai/imported-models.htm),
including NVIDIA Nemotron 3 and Nemotron 3.5 Lightning, onto a dedicated AI
cluster behind an endpoint. Point LiteLLM at the endpoint:

```python
llm = get_llm_client(
"oci/my-imported-nemotron", # any name; the endpoint decides the model
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
oci_serving_mode="DEDICATED",
oci_endpoint_id="ocid1.generativeaiendpoint.oc1..example",
oci_region="us-chicago-1",
oci_compartment_id="ocid1.compartment.oc1..example",
)
```

**Self-hosted on OKE.** Serve Nemotron with vLLM on Oracle Container Engine for
Kubernetes and use LiteLLM's `hosted_vllm/` route, exactly as in
[Local models](local-models.md):

```python
llm = get_llm_client(
"hosted_vllm/nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4",
api_base="http://127.0.0.1:8000/v1", # e.g. a kubectl port-forward to the vLLM router
)
```

A reference deployment of Nemotron 3.5 Lightning on an OKE A10 node pool is in
[NVIDIA/nvidia-oci-samples](https://github.com/NVIDIA/nvidia-oci-samples).

## Quickstart selector

The quickstart examples pick a provider from your environment. When
`OCI_COMPARTMENT_ID` is set and `NVIDIA_API_KEY` is not, they use OCI Generative AI:

| Variable | Effect |
| --- | --- |
| `OCI_COMPARTMENT_ID` | Selects OCI; passed as `oci_compartment_id` |
| `OCI_REGION` | Passed as `oci_region` (default `us-ashburn-1`) |
| `OCI_MODEL` | Model string, default `oci/meta.llama-3.3-70b-instruct` |
| `OCI_ENDPOINT_ID` | Adds `oci_serving_mode="DEDICATED"` and `oci_endpoint_id` |
| `OCI_CLI_PROFILE` | Signs requests with that `~/.oci/config` profile (API key or session token); needs `uv pip install oci` |
| `OCI_USER`, `OCI_TENANCY`, `OCI_FINGERPRINT`, `OCI_KEY_FILE` | API-key credentials read by LiteLLM when no profile is given |

With these variables set, every quickstart in `examples/quickstart/` runs on OCI
Generative AI unchanged.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

## Aliases

Put repeated configuration in `.nooa/llm_config.yaml`:

```yaml
models:
oci-llama:
model_name: oci/meta.llama-3.3-70b-instruct
temperature: 0.0
```

Registry aliases forward `model_name`, `api_base`, `api_key_env`, and a fixed set
of generation parameters to LiteLLM. Provider settings such as the region and
compartment are not part of that set, so supply them through the `OCI_REGION` and
`OCI_COMPARTMENT_ID` environment variables or as call-site keyword arguments:

```python
llm = get_llm_client("oci-llama") # OCI_* variables set
llm = get_llm_client("oci-llama", oci_compartment_id="ocid1....") # or override here
```

## Troubleshooting

- `404` or `NotAuthorizedOrNotFound`: the model is not available in `oci_region`,
or the compartment lacks a policy allowing `generative-ai-family` access.
- `oci_compartment_id is required`: set `OCI_COMPARTMENT_ID` or pass the parameter.
- Signature errors with a session-token profile: the token expires after about an
hour; run `oci session authenticate` again and rebuild the signer.

References: [LiteLLM OCI provider](https://docs.litellm.ai/docs/providers/oci),
[OCI Generative AI pretrained models](https://docs.oracle.com/en-us/iaas/Content/generative-ai/pretrained-models.htm).
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ features. Each file is standalone and includes its exact run command.
| 13 | [`13_multimodal.py`](quickstart/13_multimodal.py) | Image inputs with CodeAct and Predict | A vision-capable model |
| 14 | [`14_atif_trajectory.py`](quickstart/14_atif_trajectory.py) | Exporting ATIF trajectories for evals and downstream tooling | — |
| 15 | [`15_nemo_relay.py`](quickstart/15_nemo_relay.py) | NeMo Relay intercepts, guardrails, events, and nested generation | `uv sync --extra nemo-relay` |
| 16 | [`16_oci_generative_ai.py`](quickstart/16_oci_generative_ai.py) | OCI Generative AI as the provider through the quickstart selector: profile or API-key auth, dedicated endpoints | OCI credentials; see [OCI Generative AI](../docs/oci-generative-ai.md) |

If you are new to NOOA, run examples 1–6 in order. After that, choose by the
capability you need rather than treating the remaining files as required steps.
Expand Down
77 changes: 77 additions & 0 deletions examples/quickstart/16_oci_generative_ai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# ruff: noqa: F403,F405
"""Quickstart 16: Oracle Cloud Infrastructure (OCI) Generative AI as the model provider.

NOOA is model-agnostic through litellm, which routes ``oci/<model>`` to OCI
Generative AI. The quickstart model selector picks OCI when OCI_COMPARTMENT_ID is
set, so every quickstart in this directory runs on OCI with the same variables:

export OCI_COMPARTMENT_ID=ocid1.compartment.oc1..example
export OCI_REGION=us-chicago-1 # a region with the service
export OCI_CLI_PROFILE=DEFAULT # an ~/.oci/config profile (needs `uv pip install oci`)
# or, instead of a profile: OCI_USER, OCI_TENANCY, OCI_FINGERPRINT, OCI_KEY_FILE
uv run python examples/quickstart/16_oci_generative_ai.py

Optional: OCI_MODEL (default oci/meta.llama-3.3-70b-instruct) and OCI_ENDPOINT_ID to
target a dedicated endpoint such as an imported NVIDIA Nemotron model.

The agent is a capacity planner whose deterministic helpers are its only source
of facts. See docs/oci-generative-ai.md for explicit client construction, the
model catalog, aliases, and Nemotron options.
"""

import sys

from nooa.util.quickstart import *

if not MODEL.startswith("oci/"):
print(
f"SKIP: the quickstart selector chose {MODEL!r}. Set OCI_COMPARTMENT_ID (and OCI credentials) to run on OCI."
)
sys.exit(0)

# GPU count and total GPU memory per OCI shape. The agent reads these through the
# helper methods instead of recalling specifications from training data.
GPU_SHAPES: dict[str, dict[str, float]] = {
"VM.GPU.A10.1": {"gpus": 1, "gpu_memory_gb": 24},
"VM.GPU.A10.2": {"gpus": 2, "gpu_memory_gb": 48},
"BM.GPU.A10.4": {"gpus": 4, "gpu_memory_gb": 96},
"BM.GPU.A100-v2.8": {"gpus": 8, "gpu_memory_gb": 640},
"BM.GPU.H100.8": {"gpus": 8, "gpu_memory_gb": 640},
}


class Recommendation(BaseModel):
shape: str = Field(description="The recommended OCI GPU shape name.")
total_gpu_memory_gb: float = Field(description="Total GPU memory of that shape in GB.")
rationale: str = Field(description="One sentence explaining why this is the smallest fit.")


class CapacityPlanner(Agent, llm=llm):
"""You plan GPU capacity on Oracle Cloud for serving open-weights models."""

def gpu_shapes(self) -> dict[str, dict[str, float]]:
"""Return the available OCI GPU shapes with their GPU count and total GPU memory in GB."""
return GPU_SHAPES

def fits(self, weights_gb: float, shape: str, headroom: float = 1.25) -> bool:
"""Whether model weights, with headroom for the KV cache, fit a shape's total GPU memory."""
return weights_gb * headroom <= GPU_SHAPES[shape]["gpu_memory_gb"]

async def recommend(self, model_name: str, weights_gb: float) -> Recommendation:
"""Recommend the smallest shape whose GPU memory fits the model weights with headroom.

Use self.gpu_shapes() and self.fits() rather than recalling shape specifications.
"""
...


@autorun
async def main():
planner = CapacityPlanner()
result = await planner.recommend(
"NVIDIA Nemotron 3.5 Lightning 30B-A3B (NVFP4)", weights_gb=21.6
)
print(f"model: {MODEL}")
print(result)
53 changes: 53 additions & 0 deletions src/nooa/util/quickstart.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,45 @@
# Load environment variables
load_dotenv(override=True)


def _oci_signer_from_profile(profile: str) -> Any:
"""Build an OCI SDK request signer from an ``~/.oci/config`` profile.

Supports API-key profiles and the session-token profiles that
``oci session authenticate`` writes. litellm signs OCI Generative AI
requests with the returned object when it is passed as ``oci_signer``.
"""
try:
import oci
except ModuleNotFoundError as exc:
raise ModuleNotFoundError(
"OCI_CLI_PROFILE is set but the OCI Python SDK is not installed. "
"Install it with `uv pip install oci`, or set OCI_USER, OCI_TENANCY, "
"OCI_FINGERPRINT, and OCI_KEY_FILE instead."
) from exc

config = oci.config.from_file(profile_name=profile)
if "security_token_file" in config:
with open(os.path.expanduser(config["security_token_file"])) as f:
token = f.read().strip()
private_key = oci.signer.load_private_key_from_file(config["key_file"])
return oci.auth.signers.SecurityTokenSigner(token, private_key)
return oci.signer.Signer(
tenancy=config["tenancy"],
user=config["user"],
fingerprint=config["fingerprint"],
private_key_file_location=config["key_file"],
pass_phrase=config.get("pass_phrase"),
)


# The examples run against any litellm-supported provider. By default they pick
# whichever credential you have set (see the README's "API Keys"):
# * NVIDIA_API_KEY -> NVIDIA build.nvidia.com NIM (public), served at
# integrate.api.nvidia.com (litellm `nvidia_nim/`)
# * OPENAI_API_KEY -> OpenAI (public)
# * OCI_COMPARTMENT_ID -> Oracle Cloud Infrastructure Generative AI
# (litellm `oci/`; see docs/oci-generative-ai.md)
# * NVIDIA_INFERENCE_API_KEY -> NVIDIA internal inference gateway
# (inference-api.nvidia.com; NVIDIA employees)
# To use a specific model, set MODEL to any litellm name and provide its key,
Expand All @@ -35,6 +69,25 @@
# pass NVIDIA_API_KEY (the build.nvidia.com convention) explicitly.
MODEL = "nvidia_nim/nvidia/nemotron-3-super-120b-a12b"
llm = get_llm_client(MODEL, api_key=os.environ["NVIDIA_API_KEY"])
elif os.getenv("OCI_COMPARTMENT_ID"):
# Oracle Cloud Infrastructure (OCI) Generative AI. litellm routes `oci/*` to
# inference.generativeai.<OCI_REGION>.oci.oraclecloud.com and reads API-key
# credentials from OCI_USER, OCI_FINGERPRINT, OCI_TENANCY, and OCI_KEY_FILE
# (or OCI_KEY), or from the ~/.oci/config profile named by OCI_CLI_PROFILE.
# OCI_MODEL picks another catalog model; OCI_ENDPOINT_ID targets a dedicated
# endpoint such as an imported NVIDIA Nemotron model. See docs/oci-generative-ai.md.
MODEL = os.getenv("OCI_MODEL", "oci/meta.llama-3.3-70b-instruct")
_oci_kwargs: dict[str, Any] = {"oci_compartment_id": os.environ["OCI_COMPARTMENT_ID"]}
if os.getenv("OCI_REGION"):
_oci_kwargs["oci_region"] = os.environ["OCI_REGION"]
if os.getenv("OCI_ENDPOINT_ID"):
_oci_kwargs["oci_serving_mode"] = "DEDICATED"
_oci_kwargs["oci_endpoint_id"] = os.environ["OCI_ENDPOINT_ID"]
if os.getenv("OCI_CLI_PROFILE"):
# Reuse an ~/.oci/config profile (API key or `oci session authenticate`
# token) instead of OCI_* credential variables. Needs the `oci` SDK.
_oci_kwargs["oci_signer"] = _oci_signer_from_profile(os.environ["OCI_CLI_PROFILE"])
llm = get_llm_client(MODEL, **_oci_kwargs)
elif os.getenv("OPENAI_API_KEY"):
MODEL = "gpt-5-mini"
llm = get_llm_client(MODEL)
Expand Down
Loading