What is GreenOps AI Studio?
GreenOps AI Studio is a multi-tier SaaS platform designed to proxy and optimize LLM workloads through an OpenAI-compatible AI API Gateway. It applies semantic caching on Cloudflare Workers, FinOps telemetry for token spend and carbon footprint, and a PII anonymization API before requests reach Gemini, OpenAI, or a local GPU runtime. Teams use it as a drop-in proxy alternative to calling provider APIs directly when they need cost control, privacy, and greener inference at the edge.
Introduction & architecture
GreenOps AI is an OpenAI-compatible LLM gateway on Cloudflare Workers. Every chat request is authenticated, PII-masked, optionally served from a semantic KV cache, then routed to Gemini, OpenAI, or a local GPU runtime.
Edge Worker
TypeScript proxy at the Cloudflare PoP. OpenAI-compatible POST /v1/chat/completions.
Semantic cache
WASM prompt hash + KV GREENOPS_CACHE. HIT replies skip the GPU and cost $0 / 0 g CO₂.
PII masking
Emails, cards, phones and tax IDs are anonymized before upstream. Count is returned on X-PII-Redacted-Count.
FinOps / ESG
Response headers expose USD saved, CO₂ avoided, cache HIT/MISS and user tier.
- Cold path (MISS): trim + hash in WASM, call the LLM, write KV, emit FinOps headers.
- Hot path (HIT): KV read only — typically tens of milliseconds on the same PoP.
- Failover: Gemini 429/5xx automatically retries then falls back to OpenAI when configured.
- Chat history: each completion is appended to Cloudflare KV
GREENOPS_CHATSfor the gateway key.
Authentication & setup
Protected routes accept the gateway key as Authorization: Bearer <key> or the native header X-Proxy-Auth. For local and sandbox testing use the bootstrap key GreenOpsKey123.
Quick test key
Send Authorization: Bearer GreenOpsKey123 on chat, analytics and audit calls. Paid plans receive a gop_pro_… key after Stripe checkout. Provider keys (Gemini/OpenAI) are optional when the Worker already has server-side credentials.
| Header | Required | Meaning |
|---|---|---|
Authorization | Yes* | Bearer token. Example: Bearer GreenOpsKey123. |
X-Proxy-Auth | Yes* | Equivalent gateway key if you do not send Authorization. |
X-User-Role | No | User (default), FinOps, Compliance, Admin. |
X-User-Gemini-Key | No | Bring-your-own Gemini key. |
X-User-OpenAI-Key | No | Bring-your-own OpenAI key. |
X-Local-LLM-Url | No | OpenAI-compatible local runtime (Ollama / GPU). |
X-Webhook-Url | No | Slack / Teams webhook. POSTed asynchronously via ctx.waitUntil with FinOps (cost, tokens) and ESG (carbon offset) fields. |
X-Chat-Id | No | Existing conversation UUID. Completions also accept body chat_id. |
Copy-ready authentication
Send Authorization: Bearer <TOKEN> on every protected call. The snippets below POST a JSON chat body to /v1/chat/completions. Replace GreenOpsKey123 with your provisioned key in production.
Endpoints & API reference
Proxy URL: https://greenops-ai-proxy.copycomp-ai.workers.dev
https://greenops-ai-proxy.copycomp-ai.workers.dev
https://greenops-ai-proxy.copycomp-ai.workers.dev/v1/chat/completions
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /v1/chat/completions | Yes | OpenAI-compatible chat (JSON or SSE if stream: true). Persists the turn to KV. |
| GET | /v1/chats | Yes | List saved conversations for the gateway key |
| POST | /v1/chats | Yes | Create an empty chat |
| GET | /v1/chats/:id | Yes | Load messages of one conversation |
| DELETE | /v1/chats/:id | Yes | Delete a conversation from KV |
| GET | /health | No | Uptime probe: status, timestamp, KV semantic cache |
| GET | /v1/health | No | KV cache + analytics, Gemini / OpenAI readiness |
| GET | /v1/analytics | Yes | FinOps / ESG totals |
| GET | /v1/audit/logs | Yes | EU AI Act audit trail (Compliance / Admin) |
| POST | /v1/webhooks/stripe | Stripe-Signature | Checkout → Pro key, cancel → Free |
| GET | /v1/openapi.json | No | OpenAPI 3.0 spec |
Chat Completions
POST JSON with model + messages. Every sample sends Authorization: Bearer <TOKEN> and a JSON body. Testing uses GreenOpsKey123; swap in your provisioned key for production. Successful replies persist to KV and return X-GreenOps-Chat-Id.
Copy a language below. Each block is a ready-to-run POST to /v1/chat/completions.
Chat history & sessions
Studio conversations are stored on Cloudflare KV (GREENOPS_CHATS, fallback GREENOPS_CACHE). A D1 database is not required: chat JSON lives in KV. After each successful POST /v1/chat/completions the Worker appends the user/assistant turn, scoped to a SHA-256 of the gateway key. Pass X-Chat-Id (or body chat_id) to continue a session; the response header X-GreenOps-Chat-Id is the conversation to reuse.
- Per-key isolation: KV keys never store the Bearer secret in plaintext.
- Limits: 80 chats per user, 200 messages per chat. Title is taken from the first user prompt.
- Studio sidebar calls
GET /v1/chatsandGET /v1/chats/:idwith the same Bearer token as completions. - Bind
GREENOPS_CHATSinwrangler.toml. Completions still persist if the binding falls back toGREENOPS_CACHE.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /v1/chat/completions | Yes | Saves the turn; returns X-GreenOps-Chat-Id |
| GET | /v1/chats | Yes | { chats: [{ id, title, updatedAt, preview, messageCount }] } |
| POST | /v1/chats | Yes | Create empty chat { title? } → 201 |
| GET | /v1/chats/:id | Yes | { id, title, messages: [{ role, content, at }] } |
| DELETE | /v1/chats/:id | Yes | { deleted: true, id } |
Copy a session example: complete a chat, list history, create an empty chat, reload messages, then delete. Always send Authorization: Bearer <TOKEN>.
# 1) First turn — persist a new chat (read X-GreenOps-Chat-Id from the response)
curl -D - https://greenops-ai-proxy.copycomp-ai.workers.dev/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer GreenOpsKey123" \
-d '{
"model": "gemini-3.5-flash",
"messages": [{"role": "user", "content": "Hello GreenOps"}]
}'
# 2) Continue the same session
curl https://greenops-ai-proxy.copycomp-ai.workers.dev/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer GreenOpsKey123" \
-H "X-Chat-Id: CHAT_ID" \
-d '{
"model": "gemini-3.5-flash",
"messages": [
{"role": "user", "content": "Hello GreenOps"},
{"role": "assistant", "content": "Hi"},
{"role": "user", "content": "Continue"}
]
}'
# 3) List, load, create empty, delete
curl https://greenops-ai-proxy.copycomp-ai.workers.dev/v1/chats \
-H "Authorization: Bearer GreenOpsKey123"
curl https://greenops-ai-proxy.copycomp-ai.workers.dev/v1/chats/CHAT_ID \
-H "Authorization: Bearer GreenOpsKey123"
curl -X POST https://greenops-ai-proxy.copycomp-ai.workers.dev/v1/chats \
-H "Authorization: Bearer GreenOpsKey123" \
-H "Content-Type: application/json" \
-d '{"title":"New chat"}'
curl -X DELETE https://greenops-ai-proxy.copycomp-ai.workers.dev/v1/chats/CHAT_ID \
-H "Authorization: Bearer GreenOpsKey123"
import requests
PROXY = "https://greenops-ai-proxy.copycomp-ai.workers.dev"
TOKEN = "GreenOpsKey123"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {TOKEN}",
}
def complete(messages, chat_id=None):
h = dict(headers)
if chat_id:
h["X-Chat-Id"] = chat_id
res = requests.post(
f"{PROXY}/v1/chat/completions",
headers=h,
json={"model": "gemini-3.5-flash", "messages": messages},
timeout=60,
)
res.raise_for_status()
return res.headers.get("X-GreenOps-Chat-Id"), res.json()
chat_id, data = complete([{"role": "user", "content": "Hello GreenOps"}])
print(requests.get(f"{PROXY}/v1/chats", headers=headers, timeout=30).json())
print(requests.get(f"{PROXY}/v1/chats/{chat_id}", headers=headers, timeout=30).json())
requests.post(f"{PROXY}/v1/chats", headers=headers, json={"title": "New chat"}, timeout=30)
reply = data["choices"][0]["message"]["content"]
complete(
[
{"role": "user", "content": "Hello GreenOps"},
{"role": "assistant", "content": reply},
{"role": "user", "content": "Continue"},
],
chat_id,
)
requests.delete(f"{PROXY}/v1/chats/{chat_id}", headers=headers, timeout=30)
const PROXY = 'https://greenops-ai-proxy.copycomp-ai.workers.dev';
const TOKEN = 'GreenOpsKey123';
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${TOKEN}`,
};
async function complete(messages, chatId) {
const res = await fetch(`${PROXY}/v1/chat/completions`, {
method: 'POST',
headers: chatId ? { ...headers, 'X-Chat-Id': chatId } : headers,
body: JSON.stringify({ model: 'gemini-3.5-flash', messages }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const data = await res.json();
return {
chatId: res.headers.get('X-GreenOps-Chat-Id'),
content: data.choices?.[0]?.message?.content,
};
}
const first = await complete([{ role: 'user', content: 'Hello GreenOps' }]);
const listed = await fetch(`${PROXY}/v1/chats`, { headers });
console.log(await listed.json()); // { chats: [{ id, title, preview, ... }] }
const detail = await fetch(`${PROXY}/v1/chats/${first.chatId}`, { headers });
console.log(await detail.json()); // { messages: [{ role, content, at }] }
await fetch(`${PROXY}/v1/chats`, {
method: 'POST',
headers,
body: JSON.stringify({ title: 'New chat' }),
});
await complete([
{ role: 'user', content: 'Hello GreenOps' },
{ role: 'assistant', content: first.content },
{ role: 'user', content: 'Continue' },
], first.chatId);
await fetch(`${PROXY}/v1/chats/${first.chatId}`, { method: 'DELETE', headers });
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
const string Proxy = "https://greenops-ai-proxy.copycomp-ai.workers.dev";
const string Token = "GreenOpsKey123";
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Token);
using var first = new HttpRequestMessage(HttpMethod.Post, Proxy + "/v1/chat/completions")
{
Content = new StringContent("""
{
"model": "gemini-3.5-flash",
"messages": [{ "role": "user", "content": "Hello GreenOps" }]
}
""", Encoding.UTF8, "application/json")
};
using var firstRes = await client.SendAsync(first);
firstRes.EnsureSuccessStatusCode();
firstRes.Headers.TryGetValues("X-GreenOps-Chat-Id", out var ids);
var chatId = ids != null ? string.Join("", ids) : "";
using var cont = new HttpRequestMessage(HttpMethod.Post, Proxy + "/v1/chat/completions");
cont.Headers.TryAddWithoutValidation("X-Chat-Id", chatId);
cont.Content = new StringContent("""
{
"model": "gemini-3.5-flash",
"messages": [
{ "role": "user", "content": "Hello GreenOps" },
{ "role": "user", "content": "Continue" }
]
}
""", Encoding.UTF8, "application/json");
using var contRes = await client.SendAsync(cont);
contRes.EnsureSuccessStatusCode();
Console.WriteLine(await client.GetStringAsync(Proxy + "/v1/chats"));
Console.WriteLine(await client.GetStringAsync(Proxy + "/v1/chats/" + chatId));
using var created = await client.PostAsync(
Proxy + "/v1/chats",
new StringContent("""{"title":"New chat"}""", Encoding.UTF8, "application/json"));
using var deleted = await client.DeleteAsync(Proxy + "/v1/chats/" + chatId);
Imports System.Collections.Generic
Imports System.Net.Http
Imports System.Net.Http.Headers
Imports System.Text
Const Proxy As String = "https://greenops-ai-proxy.copycomp-ai.workers.dev"
Const Token As String = "GreenOpsKey123"
Using client As New HttpClient()
client.DefaultRequestHeaders.Authorization =
New AuthenticationHeaderValue("Bearer", Token)
Using first As New HttpRequestMessage(HttpMethod.Post, Proxy & "/v1/chat/completions")
first.Content = New StringContent(
"{""model"":""gemini-3.5-flash"",""messages"":[{""role"":""user"",""content"":""Hello GreenOps""}]}",
Encoding.UTF8, "application/json")
Dim firstRes = Await client.SendAsync(first)
firstRes.EnsureSuccessStatusCode()
Dim ids As IEnumerable(Of String) = Nothing
firstRes.Headers.TryGetValues("X-GreenOps-Chat-Id", ids)
Dim chatId = If(ids Is Nothing, "", String.Join("", ids))
Console.WriteLine(Await client.GetStringAsync(Proxy & "/v1/chats"))
Console.WriteLine(Await client.GetStringAsync(Proxy & "/v1/chats/" & chatId))
Using created = Await client.PostAsync(
Proxy & "/v1/chats",
New StringContent("{""title"":""New chat""}", Encoding.UTF8, "application/json"))
End Using
Using deleted = Await client.DeleteAsync(Proxy & "/v1/chats/" & chatId)
End Using
End Using
End Using
Stripe webhooks
Point the Stripe Dashboard endpoint at the URL below. The Worker verifies Stripe-Signature when STRIPE_WEBHOOK_SECRET is set.
https://greenops-ai-proxy.copycomp-ai.workers.dev/v1/webhooks/stripe
checkout.session.completedissues an active Pro license (gop_pro_…, 100,000 req/month).customer.subscription.deleteddeactivates the license and returns the account to Free.- Configure the endpoint in Stripe as POST /v1/webhooks/stripe. No browser CORS is required.
curl -X POST https://greenops-ai-proxy.copycomp-ai.workers.dev/v1/webhooks/stripe \
-H "Content-Type: application/json" \
-H "Stripe-Signature: t=1700000000,v1=…" \
-d '{"type":"checkout.session.completed","data":{"object":{"customer_email":"dev@acme.com"}}}'
await fetch('https://greenops-ai-proxy.copycomp-ai.workers.dev/v1/webhooks/stripe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Stripe-Signature': 't=1700000000,v1=…',
},
body: JSON.stringify({
type: 'checkout.session.completed',
data: { object: { customer_email: 'dev@acme.com' } },
}),
});
import requests
requests.post(
"https://greenops-ai-proxy.copycomp-ai.workers.dev/v1/webhooks/stripe",
headers={
"Content-Type": "application/json",
"Stripe-Signature": "t=1700000000,v1=…",
},
json={
"type": "checkout.session.completed",
"data": {"object": {"customer_email": "dev@acme.com"}},
},
timeout=30,
)
Pricing tiers & quotas
Limits are enforced at the edge. Free is daily + burst; Pro and Business are monthly; Enterprise is unlimited.
| Plan | Price | Request limits | Features |
|---|---|---|---|
| Free | $0/mo | 1,000 req/day · 30 req/min | Standard semantic caching, baseline PII masking, Community & Docs |
| Pro | $29/mo | 100,000 req/month | Priority processing, real-time ESG/FinOps telemetry, automated PII anonymization |
| Business | $149/mo | 1,000,000 req/month | ESG/FinOps PDF & CSV export, advanced PII rules, priority support |
| Enterprise | $499+/mo | Unlimited | Dedicated Workers & storage, custom ESG audit, 99.9% uptime SLA |
Error handling & status codes
The gateway returns JSON errors. Chat quota responses include error.code, error.message and error.upgrade_url.
| Status | Name | Typical cause | What to do |
|---|---|---|---|
| 401 | Unauthorized | Missing/invalid Bearer or X-Proxy-Auth. | Send Authorization: Bearer GreenOpsKey123 (test) or your provisioned key. Check for extra spaces. |
| 429 | Rate Limit Exceeded | Free 1,000/day or 30/min; Pro 100k/mo; Business 1M/mo. | Wait for Retry-After, or upgrade. Burst resets every 60 seconds. |
| 500 | Gateway Error | Unhandled Worker exception or upstream failure after retries. | Retry with backoff. Check GET /v1/health. Confirm Gemini/OpenAI keys if the model is cloud-hosted. |
| 404 | Not Found | Unknown /v1/chats/:id for this gateway key. | List chats with GET /v1/chats, then use an id from that payload. |
- 403: RBAC blocked the route (for example Audit requires Compliance/Admin) or prompt injection was rejected.
- 400: malformed JSON, missing messages[], or a required provider key is absent for that model.
- SSE: send
stream: true. Cache is bypassed on streaming responses. - 404 on /v1/chats/:id: the conversation is missing or belongs to another gateway key.
Testing & QA
GreenOps ships an automated suite so the gateway, semantic cache, chat history, and the Studio docs modal stay copy-ready. Backend tests run in Vitest against an in-memory mock Worker (or a live origin when GREENOPS_URL is set). Frontend tests run in Playwright against the static Studio server on port 4173.
- Install once:
npx playwright install chromium(needed for E2E). Vitest is a local npm dependency. - Default API tests never call production:
tests/helpers/mock-greenops-worker.jsmocks completions, KV cache HIT/MISS, and/v1/chats. - Point API tests at a live Worker with
GREENOPS_URLand optionalGREENOPS_AUTH.
Backend · Vitest
tests/api.spec.js covers the OpenAI-compatible gateway: missing Bearer returns 401; a valid key returns 200 with FinOps/ESG headers; an identical prompt is a semantic-cache MISS then HIT; GET/POST/DELETE /v1/chats persist and drop history. Run with npm run test:api (vitest run tests/api.spec.js).
Frontend · Playwright
tests/e2e/docs.spec.js opens Studio, clicks Documentation, fetches same-origin /docs.html without redirect loops, and checks the copyable language tabs (cURL, Python, JavaScript/TypeScript, C#, VB.Net) in English and Italian. Playwright starts node scripts/studio-static.mjs on port 4173. Run with npm run test:e2e.
Quick commands
From the repository root. npm test runs API tests first, then E2E.
| Command | What it runs |
|---|---|
npm test | Runs all tests |
npm run test:api | Runs the API tests |
npm run test:e2e | Runs the E2E tests |
npm test
npm run test:api
npm run test:e2e
Contact & support
Reach GreenOps Studio by email. Technical issues go to support; general inquiries to info; Enterprise and commercial requests to sales.
- Technical support: support@greenopsstudio.com
- General info: info@greenopsstudio.com
- Sales / Enterprise: sales@greenopsstudio.com