Portkey + Meridian
Forward Portkey Logs Export records to Meridian gateway ingest on a schedule, attribute them by agent id metadata, and verify usage in the ledger.
Available to customers now. We confirm the first import with you in a sandbox before production.
Best for: Teams using Portkey as an AI gateway
Portkey + Meridian gateway ingest
Egress: api.onaro.io:443 (HTTPS)
What it does
Portkey records every request routed through it: the provider, the model, request and response tokens, the cost, and any metadata the caller attached. Meridian accepts Portkey log records at a dedicated ingest endpoint, maps each one to the Open Agent Spend Attribution (OASA) 0.2.0 usage record, ties it to an agent, and rolls it into the spend ledger every hour.
Portkey does not push its logs to Meridian on its own. The path that works today is a small job you run on a schedule: export logs from Portkey with its Logs Export API, reshape each line into the record Meridian reads, and post the records to Meridian in batches. This guide includes that job as a complete script.
You run Portkey and the job. Meridian stores no Portkey credentials, and the script sends only usage fields, never prompts or responses.
There is no Connect card for Portkey in the Meridian app.
Prerequisites
- Portkey Logs Export. The script uses Portkey's Logs Export API (verified as of 2026-10-07). Check that your Portkey plan includes Logs Export, and create a Portkey API key with the
logs.exportscope. - Agent ids in Portkey metadata. Have each agent or application send its Meridian agent id as Portkey metadata on every request, for example the header
x-portkey-metadata: {"agent_id": "support-triage", "cost_center": "CC-4100"}. This is how the exported records are attributed. - A Meridian ingest key. An organization admin creates it in Meridian: open Settings → Usage ingest keys, select Create key, name it after this export job (for example "Portkey export"), and copy the key from the dialog. The key has the
meridian:ingestscope only. The keys created on the Discovery event keys page are a different kind and are rejected by this endpoint with 401. The key is shown once; store it in your secret manager. See Manage ingest keys. - Somewhere to run the job: any scheduler that can run Python 3.10 or later with the
requestspackage and reach bothapi.portkey.aiandapi.onaro.ioover HTTPS.
Setup
1. Confirm the endpoint
POST https://api.onaro.io/v1/meridian/ingest/gateways/portkey Authorization: Bearer MERIDIAN_INGEST_KEY Content-Type: application/json
The body is one record or a JSON array of records, up to 1,000 per request.
2. Export logs from Portkey
Create an export for the window you want, start it, wait for it to finish, and download the JSON Lines file. Request at least these fields: id, trace_id, created_at, ai_provider, ai_model, request_tokens, response_tokens, cost, status_code and metadata. Leave out request and response; Meridian does not need message content. One Portkey export job returns up to 50,000 logs, so split busy days into smaller windows.
3. Reshape and post the records
Save this as portkey_to_meridian.py. It reads the exported file, skips failed requests, converts Portkey's cost from cents to dollars, and posts batches of 500.
"""Forward a Portkey Logs Export (JSON Lines) to Meridian gateway ingest.""" import json import os import sys import requests MERIDIAN_URL = "https://api.onaro.io/v1/meridian/ingest/gateways/portkey" MERIDIAN_KEY = os.environ["MERIDIAN_INGEST_KEY"] BATCH = 500 def number(value): if value in (None, ""): return None parsed = float(value) return int(parsed) if parsed.is_integer() else parsed def reshape(log): status = number(log.get("status_code") or log.get("response_status_code")) if status is not None and status >= 400: return None metadata = log.get("metadata") or {} if isinstance(metadata, str): metadata = json.loads(metadata or "{}") cost_cents = number(log.get("cost")) record = { "id": log["id"], "trace_id": log.get("trace_id"), "created_at": log["created_at"], "config": { "provider": log.get("ai_provider") or log.get("ai_org"), "model": log.get("ai_model"), }, "response": { "body": { "usage": { "prompt_tokens": number(log.get("request_tokens") or log.get("req_units")), "completion_tokens": number(log.get("response_tokens") or log.get("res_units")), } }, }, "metadata": { "agent_id": metadata.get("agent_id"), "cost_center": metadata.get("cost_center"), }, } if cost_cents is not None: record["response"]["cost"] = cost_cents / 100 return record def post(batch): r = requests.post( MERIDIAN_URL, headers={"Authorization": f"Bearer {MERIDIAN_KEY}"}, json=batch, timeout=60, ) r.raise_for_status() body = r.json() if "error" in body: sys.exit(f"Meridian refused the batch: {body}") print(f"accepted={body['accepted']} duplicates={body['duplicates']} rejected={len(body['rejected'])}") for item in body["rejected"]: print(f" rejected index={item['index']} field={item['field']} reason={item['reason']}") def main(path): batch = [] with open(path, encoding="utf-8") as f: for line in f: if not line.strip(): continue record = reshape(json.loads(line)) if record: batch.append(record) if len(batch) == BATCH: post(batch) batch = [] if batch: post(batch) if __name__ == "__main__": main(sys.argv[1])
Run it against each export:
export MERIDIAN_INGEST_KEY=bai_... python portkey_to_meridian.py logs_export.jsonl
| Placeholder | Value |
|---|---|
MERIDIAN_INGEST_KEY | The meridian:ingest key created in Settings → Usage ingest keys |
logs_export.jsonl | The file downloaded from Portkey's Logs Export |
4. Schedule it
Run the export and the script at least once a day, covering the previous day. Meridian drops replays, so overlapping windows are safe; gaps are not, because records that arrive more than 7 days after the request are stored but never rolled up.
A simple pattern is a daily job shortly after midnight UTC that exports the previous two days, downloads the file and runs the script. The overlap covers late-arriving Portkey logs and a missed run, and the duplicates count in the output tells you how much of the window Meridian had already seen. Keep the downloaded files for a few days so you can re-send a window if a run fails.
5. Optional: map key refs in the registry
Meridian can also attribute Portkey records by virtual_key or api_key_id through the Gateway key registry at /meridian/settings/gateway-keys (open it by path; it is not linked from Settings yet). Portkey's Logs Export does not include either field, so this only applies if you send records from another source that does.
What Meridian receives
| Meridian field | Record field (as reshaped by the script) | Portkey export field |
|---|---|---|
| Record id | id (turned into a stable UUID) | id |
| Occurred at (required) | created_at | created_at |
| Provider | config.provider | ai_provider |
| Model | config.model | ai_model |
| Input tokens | response.body.usage.prompt_tokens | request_tokens |
| Output tokens | response.body.usage.completion_tokens | response_tokens |
| Cost | response.cost (USD) | cost (cents, divided by 100) |
| Agent id | metadata.agent_id | metadata.agent_id |
| Cost center | metadata.cost_center | metadata.cost_center |
| Operation | request.type, lowercased; chat when absent | not sent |
Records are written as usage records with source_system gateway:portkey and currency USD. Numbers must be sent as numbers; the script converts them.
Cost. When the record has a cost, Meridian uses it. When it does not, Meridian applies your organization's rate card (Settings → Rate cards), then its model price catalog, and records $0 only if neither has the model.
Timing. The rollup into the spend ledger runs every hour at 20 minutes past, in UTC, and looks back 7 days.
Verification
- Send a test request through Portkey with
agent_idin its metadata. - Export a window that includes it and run the script.
- Read the script output. Meridian returns HTTP 200 for any well-formed batch, even when every record is rejected. The script prints
accepted,duplicatesand each rejection with its field and reason. A good run showsaccepted=1 duplicates=0 rejected=0. - Check the health endpoint with the same key:
GET https://api.onaro.io/v1/meridian/ingest/healthreturnslast_ingest_at, totals, the duplicate rate and a key expiry warning. - Check the ledger after the next rollup: the request appears under the agent on Agents & allocation.
- Run the script again on the same file. Every record should come back as a duplicate.
What gets attributed and what doesn't
- Attributed: records whose metadata carries an
agent_id. Meridian matches it to the agent with that id, and creates the agent if none exists yet; rename it and give it a department on the Agents page. - Unattributed: records with no
agent_idin metadata. They roll up as unattributed and appear in the attribution inbox (Agents & allocation → Unattributed), where you can assign them by hand. Manual assignments survive later rollups. - Declared fields are limited to
agent_idandcost_center. Pool and endpoint ids are not read from Portkey records. - Cost center is stored with the raw event but not written to the ledger, so it does not drive department reporting.
- Failed requests are skipped by the script. Meridian itself does not filter by status, so other forwarders should skip them too.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| 401 "API key required" | The key is not a meridian:ingest key. Create an ingest key in Settings → Usage ingest keys. |
| 401 "Invalid API key", "revoked" or "expired" | Create a new key in Settings → Usage ingest keys, update MERIDIAN_INGEST_KEY, then revoke the old key. |
| 403 "Scope 'meridian:ingest' required" | The scope must be listed exactly; meridian:* does not imply it. |
429 with Retry-After | Limits count HTTP requests per key, per minute and per day; the lowest key tier allows 20 a minute and 100 a day. Each script batch is one request, so 500 records cost one request. |
"Meridian refused the batch" with batch_too_large | More than 1,000 records in one request. Keep BATCH at 1,000 or less. |
Rejections with field: "occurred_at" | The export line had no created_at. Add it to the export's requested fields. |
| Tokens are zero | The export did not include request_tokens and response_tokens. Add them to the requested fields. |
| Everything is unattributed | Requests are not carrying agent_id in Portkey metadata, or metadata was left out of the export. |
| Portkey refuses the export | The Portkey key lacks the logs.export scope, or an admin has blocked a requested field from export. |
Limits
- Early access. Meridian's Portkey mapping is pinned to Portkey's log record shape at 1.10.0 and was built from Portkey's documentation. Check the script output for rejections before relying on totals.
- Batch, not real time. Data is as fresh as your export schedule.
- No push path. Portkey's webhooks and OpenTelemetry export are not supported; their payloads differ from the record Meridian reads.
- Ingest keys are managed by organization admins. Up to 10 active ingest keys per organization. See Manage ingest keys.
- HTTP 200 does not mean accepted. Read
acceptedandrejected; the script does. - Registry attribution needs a key ref that Portkey's export does not include, so attribution relies on
agent_idmetadata. - Late data. Records that arrive more than 7 days after the request are stored but not rolled up.