first commit

This commit is contained in:
2026-02-12 10:29:44 -03:00
commit b5da096676
3 changed files with 688 additions and 0 deletions

306
README.md Normal file
View File

@@ -0,0 +1,306 @@
# Integrating OpenClaw with Oracle Cloud Generative AI (OCI)
## Overview
This tutorial explains how to integrate **OpenClaw** with **Oracle Cloud
Infrastructure (OCI) Generative AI** by building an OpenAI-compatible
API gateway using FastAPI.
Instead of modifying OpenClaw's core, we expose an **OpenAI-compatible
endpoint** (`/v1/chat/completions`) that internally routes requests to
OCI Generative AI.
This approach provides:
- ✅ Full OpenClaw compatibility
- ✅ Control over OCI model mapping
- ✅ Support for streaming responses
- ✅ Enterprise-grade OCI infrastructure
- ✅ Secure request signing via OCI SDK
------------------------------------------------------------------------
# Why Use OCI Generative AI?
Oracle Cloud Infrastructure provides:
- Enterprise security (IAM, compartments, VCN)
- Flexible model serving (ON_DEMAND, Dedicated)
- High scalability
- Cost control
- Regional deployment control
- Native integration with Oracle ecosystem
By building an OpenAI-compatible proxy, we combine:
OpenClaw flexibility + OCI enterprise power
------------------------------------------------------------------------
# Architecture
OpenClaw ↓ OpenAI-Compatible Gateway (FastAPI) ↓ OCI Generative AI REST
API (20231130) ↓ OCI Hosted LLM
------------------------------------------------------------------------
# Project Structure
project/
├── oci_openai_proxy.py
├── README.md
------------------------------------------------------------------------
# Key Code Sections Explained
## 1⃣ Configuration Section
``` python
OCI_CONFIG_FILE = os.getenv("OCI_CONFIG_FILE", os.path.expanduser("~/.oci/config"))
OCI_PROFILE = os.getenv("OCI_PROFILE", "DEFAULT")
OCI_COMPARTMENT_ID = os.getenv("OCI_COMPARTMENT_ID", "...")
OCI_GENAI_ENDPOINT = os.getenv(
"OCI_GENAI_ENDPOINT",
"https://inference.generativeai.us-chicago-1.oci.oraclecloud.com"
)
```
### What it does:
- Reads OCI authentication config
- Defines target compartment
- Defines the OCI inference endpoint
------------------------------------------------------------------------
## 2⃣ Model Mapping
``` python
MODEL_MAP = {
"gpt-4o-mini": "openai.gpt-4.1",
"text-embedding-3-small": "cohere.embed-multilingual-v3.0",
}
```
### Why this is important:
OpenClaw expects OpenAI model names.\
OCI uses different model IDs.
This dictionary translates between them.
------------------------------------------------------------------------
## 3⃣ Pydantic OpenAI-Compatible Request Model
``` python
class ChatRequest(BaseModel):
model: str
messages: List[Message]
temperature: Optional[float] = None
max_tokens: Optional[int] = None
stream: Optional[bool] = False
```
### Purpose:
Defines a request format fully compatible with OpenAI's API.
------------------------------------------------------------------------
## 4⃣ OCI Signer
``` python
def get_signer():
config = oci.config.from_file(OCI_CONFIG_FILE, OCI_PROFILE)
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"),
)
return signer
```
### Purpose:
Creates a signed request for OCI REST calls.
Without this, OCI rejects the request.
------------------------------------------------------------------------
## 5⃣ Message Conversion (OpenAI → OCI Format)
``` python
def openai_to_oci_messages(messages: list, model_id: str) -> list:
```
OCI expects:
{
"role": "USER",
"content": [
{"type": "TEXT", "text": "..."}
]
}
OpenAI sends:
{ "role": "user", "content": "..." }
This function converts formats.
------------------------------------------------------------------------
## 6⃣ OCI REST Call
``` python
url = f"{OCI_GENAI_ENDPOINT}/20231130/actions/chat"
```
We use OCI's REST endpoint:
POST /20231130/actions/chat
Payload structure:
{
"compartmentId": "...",
"servingMode": {
"servingType": "ON_DEMAND",
"modelId": "openai.gpt-4.1"
},
"chatRequest": {
"apiFormat": "GENERIC",
"messages": [...],
"maxTokens": 512
}
}
------------------------------------------------------------------------
## 7⃣ Streaming Implementation
``` python
def fake_stream(text: str, model: str):
```
Since OCI GENERIC mode returns full response (not streaming), we
simulate OpenAI streaming by splitting the response into chunks.
This keeps OpenClaw fully compatible.
------------------------------------------------------------------------
## 8⃣ OpenAI-Compatible Response Builder
``` python
def build_openai_response(model: str, text: str)
```
Formats the OCI response to match OpenAI's schema:
{
"id": "...",
"object": "chat.completion",
"choices": [...]
}
------------------------------------------------------------------------
# Running the Server
Install dependencies:
pip install fastapi uvicorn requests oci pydantic
Run:
uvicorn oci_openai_proxy:app --host 0.0.0.0 --port 8050
------------------------------------------------------------------------
# Testing with curl
curl http://127.0.0.1:8050/v1/chat/completions -H "Content-Type: application/json" -d '{
"model": "gpt-4o-mini",
"messages": [
{"role": "user", "content": "Hello"}
]
}'
------------------------------------------------------------------------
# OpenClaw Configuration (openclaw.json)
Edit your **openclaw.json** configuration file (normaly it's in ~/.openclaw/openclaw.json) and replace models and agents definitions with:
```json
{
"models":{
"providers":{
"openai-compatible":{
"baseUrl":"http://127.0.0.1:8050/v1",
"apiKey":"sk-test",
"api":"openai-completions",
"models":[
{
"id":"gpt-4o-mini",
"name":"gpt-4o-mini",
"reasoning":false,
"input":[
"text"
],
"contextWindow":200000,
"maxTokens":8192
}
]
}
}
},
"agents":{
"defaults":{
"model":{
"primary":"openai-compatible/gpt-4o-mini"
}
}
},
"gateway":{
"port":18789,
"mode":"local",
"bind":"loopback"
}
}
```
### Important Fields
```table
Field Purpose
--------------- --------------------------------
baseUrl Points OpenClaw to our gateway
api Must be openai-completions
model id Must match MODEL_MAP key
contextWindow Model context size
maxTokens Max response tokens
```
------------------------------------------------------------------------
# Final Notes
You now have:
✔ OpenClaw fully integrated\
✔ OCI Generative AI backend\
✔ Streaming compatibility\
✔ Enterprise-ready architecture
------------------------------------------------------------------------
# Acknowledgments
- **Author** - Cristiano Hoshikawa (Oracle LAD A-Team Solution Engineer)

316
oci_openapi_proxy.py Normal file
View File

@@ -0,0 +1,316 @@
import os
import time
import json
import uuid
from typing import List, Optional, Dict, Any, Iterable
import requests
import oci
from fastapi import FastAPI, HTTPException
from fastapi import Request
from fastapi.responses import JSONResponse, StreamingResponse
from pydantic import BaseModel, ConfigDict
# ============================================================
# CONFIG
# ============================================================
OCI_CONFIG_FILE = os.getenv("OCI_CONFIG_FILE", os.path.expanduser("~/.oci/config"))
OCI_PROFILE = os.getenv("OCI_PROFILE", "DEFAULT")
OCI_COMPARTMENT_ID = os.getenv("OCI_COMPARTMENT_ID", "<YOUR_COMPARTMENT_ID>")
OCI_GENAI_ENDPOINT = os.getenv(
"OCI_GENAI_ENDPOINT",
"https://inference.generativeai.<region>.oci.oraclecloud.com"
)
MODEL_MAP = {
"gpt-4o-mini": "openai.gpt-4.1",
"text-embedding-3-small": "cohere.embed-multilingual-v3.0",
}
app = FastAPI(title="OCI OpenAI-Compatible Gateway")
# ============================================================
# Pydantic Models (OpenAI-compatible)
# ============================================================
class Message(BaseModel):
role: str
content: str
class EmbeddingRequest(BaseModel):
model: str
input: List[str] | str
class ChatRequest(BaseModel):
model: str
messages: List[Message]
temperature: Optional[float] = None
max_tokens: Optional[int] = None
max_completion_tokens: Optional[int] = None
top_p: Optional[float] = None
stream: Optional[bool] = False
tools: Optional[Any] = None
tool_choice: Optional[Any] = None
model_config = ConfigDict(extra="allow")
# ============================================================
# OCI SIGNER
# ============================================================
def get_signer():
config = oci.config.from_file(OCI_CONFIG_FILE, OCI_PROFILE)
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"),
)
return signer
# ============================================================
# CONVERSION HELPERS
# ============================================================
def openai_to_oci_messages(messages: list, model_id: str) -> list:
oci_messages = []
for m in messages:
role = m.get("role", "").upper()
if role == "SYSTEM":
role = "SYSTEM"
elif role == "ASSISTANT":
role = "ASSISTANT"
else:
role = "USER"
oci_messages.append({
"role": role,
"content": [
{
"type": "TEXT",
"text": m.get("content", "")
}
]
})
return oci_messages
def build_openai_response(model: str, text: str) -> Dict[str, Any]:
return {
"id": f"chatcmpl-{uuid.uuid4().hex}",
"object": "chat.completion",
"created": int(time.time()),
"model": model,
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": text,
},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": None,
"completion_tokens": None,
"total_tokens": None,
},
}
def normalize_messages(messages: list) -> list:
normalized = []
for m in messages:
content = m.get("content")
# Caso OpenClaw envie array [{type:"text", text:"..."}]
if isinstance(content, list):
text_parts = []
for item in content:
if isinstance(item, dict) and item.get("type") == "text":
text_parts.append(item.get("text", ""))
content = "\n".join(text_parts)
normalized.append({
"role": m.get("role"),
"content": content
})
return normalized
def fake_stream(text: str, model: str):
completion_id = f"chatcmpl-{uuid.uuid4().hex}"
created = int(time.time())
yield f"data: {json.dumps({
'id': completion_id,
'object': 'chat.completion.chunk',
'created': created,
'model': model,
'choices': [{
'index': 0,
'delta': {'role': 'assistant'},
'finish_reason': None
}]
})}\n\n"
for i in range(0, len(text), 40):
chunk = text[i:i+40]
yield f"data: {json.dumps({
'id': completion_id,
'object': 'chat.completion.chunk',
'created': created,
'model': model,
'choices': [{
'index': 0,
'delta': {'content': chunk},
'finish_reason': None
}]
})}\n\n"
yield "data: [DONE]\n\n"
# ============================================================
# OCI CHAT CALL (REST 20231130)
# ============================================================
def call_oci_chat(request: dict) -> str:
signer = get_signer()
model = request.get("model")
oci_model = MODEL_MAP.get(model, model)
url = f"{OCI_GENAI_ENDPOINT}/20231130/actions/chat"
oci_messages = []
for m in request.get("messages", []):
oci_messages.append({
"role": m["role"].upper(),
"content": [
{
"type": "TEXT",
"text": m["content"]
}
]
})
payload = {
"compartmentId": OCI_COMPARTMENT_ID,
"servingMode": {
"servingType": "ON_DEMAND",
"modelId": oci_model
},
"chatRequest": {
"apiFormat": "GENERIC",
"messages": oci_messages,
"maxTokens": request.get("max_tokens", 512),
"temperature": request.get("temperature", 0.7),
"topP": request.get("top_p", 0.9)
}
}
print("\n================ OCI PAYLOAD ================")
print(json.dumps(payload, indent=2, ensure_ascii=False))
print("=============================================\n")
response = requests.post(
url,
json=payload,
auth=signer,
headers={"Content-Type": "application/json"},
)
if response.status_code != 200:
print("\n================ OCI ERROR =================")
print(response.text)
print("===========================================\n")
raise HTTPException(status_code=500, detail=response.text)
data = response.json()
# Caminho correto da resposta GENERIC
choices = data["chatResponse"]["choices"]
message = choices[0]["message"]
content = message["content"]
return content[0]["text"]
# ============================================================
# ENDPOINTS
# ============================================================
@app.get("/v1/models")
def list_models():
return {
"object": "list",
"data": [
{"id": k, "object": "model", "owned_by": "oci"}
for k in MODEL_MAP.keys()
],
}
@app.post("/v1/chat/completions")
async def chat_completions(request: Request):
body = await request.json()
print("\n=== OPENCLAW BODY ===")
print(json.dumps(body, indent=2))
print("=====================\n")
body["messages"] = normalize_messages(body["messages"])
text = call_oci_chat(body)
if body.get("stream"):
return StreamingResponse(
fake_stream(text, body["model"]),
media_type="text/event-stream"
)
return build_openai_response(body["model"], text)
# ============================================================
# HEALTHCHECK
# ============================================================
@app.get("/health")
def health():
return {"status": "ok"}
@app.middleware("http")
async def log_requests(request: Request, call_next):
body = await request.body()
try:
body_json = json.loads(body.decode())
except:
body_json = body.decode()
print("\n>>> HIT:", request.method, request.url.path)
print(">>> BODY:", json.dumps(body_json, indent=2, ensure_ascii=False))
# NÃO mexe no request._receive
response = await call_next(request)
return response
@app.post("/v1/responses")
async def responses_passthrough(request: Request):
body = await request.json()
body["messages"] = normalize_messages(body.get("messages", []))
text = call_oci_chat(body)
if body.get("stream"):
return StreamingResponse(
fake_stream(text, body["model"]),
media_type="text/event-stream"
)
return build_openai_response(body["model"], text)

66
openclaw.json Normal file
View File

@@ -0,0 +1,66 @@
{
"meta": {
"lastTouchedVersion": "2026.2.6-3",
"lastTouchedAt": "2026-02-12T10:50:18.766Z"
},
"wizard": {
"lastRunAt": "2026-02-12T10:50:18.763Z",
"lastRunVersion": "2026.2.6-3",
"lastRunCommand": "onboard",
"lastRunMode": "local"
},
"models": {
"providers": {
"openai-compatible": {
"baseUrl": "http://127.0.0.1:8050/v1",
"apiKey": "sk-test",
"api": "openai-completions",
"models": [
{
"id": "gpt-4o-mini",
"name": "gpt-4o-mini",
"reasoning": false,
"input": ["text"],
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"contextWindow": 200000,
"maxTokens": 8192
}
]
}
}
},
"agents": {
"defaults": {
"model": {
"primary": "openai-compatible/gpt-4o-mini"
}
}
},
"messages": {
"ackReactionScope": "group-mentions"
},
"commands": {
"native": "auto",
"nativeSkills": "auto"
},
"gateway": {
"port": 18789,
"mode": "local",
"bind": "loopback",
"auth": {
"mode": "token",
"token": "3c758e93a4e6bbdd823795a4e40a70409099823f49b89b93"
},
"tailscale": {
"mode": "off",
"resetOnExit": false
}
},
"plugins": {
"entries": {
"whatsapp": {
"enabled": true
}
}
}
}