mirror of
https://github.com/hoshikawa2/openclaw-oci.git
synced 2026-03-06 18:21:00 +00:00
first commit
This commit is contained in:
306
README.md
Normal file
306
README.md
Normal 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
316
oci_openapi_proxy.py
Normal 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
66
openclaw.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user