🗝️

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.

Early accessShipped: 2026-09-22Time: 1-2 hoursDifficulty: Intermediate

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.export scope.
  • 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:ingest scope 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 requests package and reach both api.portkey.ai and api.onaro.io over 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
PlaceholderValue
MERIDIAN_INGEST_KEYThe meridian:ingest key created in Settings → Usage ingest keys
logs_export.jsonlThe 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 fieldRecord field (as reshaped by the script)Portkey export field
Record idid (turned into a stable UUID)id
Occurred at (required)created_atcreated_at
Providerconfig.providerai_provider
Modelconfig.modelai_model
Input tokensresponse.body.usage.prompt_tokensrequest_tokens
Output tokensresponse.body.usage.completion_tokensresponse_tokens
Costresponse.cost (USD)cost (cents, divided by 100)
Agent idmetadata.agent_idmetadata.agent_id
Cost centermetadata.cost_centermetadata.cost_center
Operationrequest.type, lowercased; chat when absentnot 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

  1. Send a test request through Portkey with agent_id in its metadata.
  2. Export a window that includes it and run the script.
  3. Read the script output. Meridian returns HTTP 200 for any well-formed batch, even when every record is rejected. The script prints accepted, duplicates and each rejection with its field and reason. A good run shows accepted=1 duplicates=0 rejected=0.
  4. Check the health endpoint with the same key: GET https://api.onaro.io/v1/meridian/ingest/health returns last_ingest_at, totals, the duplicate rate and a key expiry warning.
  5. Check the ledger after the next rollup: the request appears under the agent on Agents & allocation.
  6. 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_id in 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_id and cost_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

SymptomCause 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-AfterLimits 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_largeMore 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 zeroThe export did not include request_tokens and response_tokens. Add them to the requested fields.
Everything is unattributedRequests are not carrying agent_id in Portkey metadata, or metadata was left out of the export.
Portkey refuses the exportThe 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 accepted and rejected; the script does.
  • Registry attribution needs a key ref that Portkey's export does not include, so attribution relies on agent_id metadata.
  • Late data. Records that arrive more than 7 days after the request are stored but not rolled up.