SPB Git forge

spb/doc-api

Public
2commits 1branches 0releases
15.7 MBsize
maindefault branch
13 days agolast push
Python 88.3% TypeScript 7.6% Shell 4.1%
17.8 KB · 194 lines python
Raw Blame History
1#!/usr/bin/env python32"""Render docs/openai/admin-api.md from the generated admin fragments (tables) + curated prose."""3from __future__ import annotations4import json5from collections import OrderedDict6from pathlib import Path78ROOT = Path(__file__).resolve().parents[2]9E = json.loads((ROOT / "generated/fragments/endpoints/openai-admin.json").read_text())10P = json.loads((ROOT / "generated/fragments/parameters/openai-admin.json").read_text())11A = json.loads((ROOT / "generated/fragments/audit-log-events/openai-audit-log-events.json").read_text())1213def group_of(path: str) -> str:14    p = path.replace("/v1/", "")15    if p.startswith("organization/usage") or p == "organization/costs": return "Usage & costs"16    if "/certificates" in p: return "Certificates (Mutual TLS)"17    if p.startswith("organization/projects/{project_id}/"):18        sub = p.split("/")[3]19        return "Project " + sub.replace("_", " ")20    if p.startswith("projects/"): return "Project roles & role bindings"21    if p.startswith("organization/users/{user_id}/roles") or p.startswith("organization/groups/{group_id}/roles"): return "Organization role bindings"22    if p.startswith("organization/groups/{group_id}/users"): return "Group membership"23    return "Organization " + p.split("/")[1].replace("_", " ")2425groups: "OrderedDict[str, list]" = OrderedDict()26for e in E:27    groups.setdefault(group_of(e["path"]), []).append(e)2829def st(e):30    s = e["status"]31    return "ACCOUNT_RESTRICTED (403/401 with project key)" if "ACCOUNT_RESTRICTED" in s else ", ".join(s)3233def params_for(e):34    eid = f"{e['method']} {e['path']}"35    ps = [p for p in P if p["endpoint"] == eid and p["location"] in ("query", "body") and "." not in p["parameter"] and "[]" not in p["parameter"]]36    out = []37    for p in ps:38        tag = "*" if p["required"] else ""39        extra = f" ({'|'.join(map(str, p['enum']))})" if p.get("enum") and len(p["enum"]) <= 8 else ""40        out.append(f"`{p['parameter']}`{tag}{extra}")41    return ", ".join(out) or "—"4243lines = []44w = lines.append45w("# OpenAI Administration API — atlas\n")46w("**Status:** `DOCUMENTED` for all 124 operations (OpenAPI spec v2.3.0); every GET we could probe is `ACCOUNT_RESTRICTED` (our project key gets HTTP 403 — 401 for audit logs — `Missing scopes: api.management.read` etc.). No Admin key is available, so nothing below is `LIVE_VERIFIED` except the exact error shapes. No mutating admin call was ever issued.\n")47w("**Sources:** [Administration overview](https://developers.openai.com/api/reference/administration/overview) · [Admin APIs guide](https://developers.openai.com/api/docs/guides/admin-apis) · [Production best practices → organization](https://developers.openai.com/api/docs/guides/production-best-practices#setting-up-your-organization) · [Spend limits](https://developers.openai.com/api/docs/guides/spend-limits) · [Mutual TLS](https://developers.openai.com/api/docs/guides/mutual-tls) · [IP allowlist](https://developers.openai.com/api/docs/guides/ip-allowlist) · [Your data (retention / residency)](https://developers.openai.com/api/docs/guides/your-data) · OpenAPI `sources/openai/openapi/openapi-master.yaml` (all `/organization/*` + `/projects/*` paths) · SDK surfaces `sources/openai/openapi/{python,node}-sdk-api.md`.\n")48w("**Last verified:** 2026-09-18 (live probes: 14 admin GETs, all 401/403; details in `tmp-live/openai-admin/` and `reports/live-requests.jsonl`).\n")49w("Machine-readable twins: `generated/fragments/endpoints/openai-admin.json` (124 endpoints), `generated/fragments/parameters/openai-admin.json` (431 parameters), `generated/fragments/audit-log-events/openai-audit-log-events.json` (149 event types).\n")50w("""51## 1. Model of the Admin API5253| Concept | What it is | Where |54|---|---|---|55| Organization | Billing + policy boundary; has users (owner/reader), groups, custom roles, spend limit, data retention, certificates, external storage, audit log | `/v1/organization/*` |56| Project | Isolation unit inside an org: own API keys, service accounts, users (owner/member), groups, per-model rate limits, model/hosted-tool permissions, spend limit & alerts, data retention, certificates, webhooks | `/v1/organization/projects/{project_id}/*`, project roles at `/v1/projects/{project_id}/*` |57| Admin API key | `sk-admin-…` created by org owners at *Settings → Organization → Admin keys*; the only credential accepted by `/v1/organization/*` and `/v1/projects/*`; refused by non-admin endpoints. Workload-identity tokens **cannot** call Admin endpoints. | `GET/POST/DELETE /v1/organization/admin_api_keys` |58| Scopes | Fine-grained permissions carried by restricted keys / roles. Read scopes we observed in error messages: `api.management.read` (users, invites, projects, admin keys, spend limit/alerts, data retention), `api.usage.read` (usage + costs), `api.audit_logs.read`, `api.roles.read`, `api.groups.read`, `api.mtls.read` (certificates, message adds "(Owner)"), `api.external_storage.read`. Documented write scopes: `api.groups.write`, `api.mtls.write`. Model-side scopes seen in the spec/guides: `api.model.request`, `api.model.read`, `api.responses.write`, `api.files.read/write`, `api.batch.read/write`, `api.vector_store.read`, `api.videos.read/write`, `api.voices.read/write`, `api.agents.read/write`, `api.vaults.read/write`, `api.webhooks.read`, `api.traces.read`, `api.safety.read`, `api.safety.alerts.read`, `api.apps.read/write`. | error bodies, Role.permissions[] |59| Roles | Predefined + custom roles (`object: role`, `permissions[]`, `resource_type: api.organization | api.project`, `predefined_role`). Bound to users or groups at org level (`/organization/users/{id}/roles`, `/organization/groups/{id}/roles`) or project level (`/projects/{id}/users/{uid}/roles`, `/projects/{id}/groups/{gid}/roles`). Legacy simple roles: org `owner` / `reader`; project `owner` / `member`. | `/v1/organization/roles`, `/v1/projects/{id}/roles` |60| Pagination | Cursor style: `limit` (1–100, default 20) + `after` → `{object:"list", data, first_id, last_id, has_more}`. Usage/costs use `page` → `{object:"page", data:[buckets], has_more, next_page}`. Roles/groups lists use `after`/`limit` and return `has_more` + `next` cursor (SDK `SyncNextCursorPage`). | — |61| Mutations | Always `POST` (no PUT/PATCH), deletes are `DELETE` → `{object:"…deleted", id, deleted:true}`. No Idempotency-Key header. | — |6263## 2. Endpoint catalogue6465Legend: `*` = required parameter; SDK column = Python method (Node is the camelCase twin, e.g. `client.admin.organization.auditLogs.list`).66""")67for g, eps in groups.items():68    w(f"\n### {g}\n")69    w("| Method | Path | Name | Key params (query/body) | Python SDK | Status |")70    w("|---|---|---|---|---|---|")71    for e in eps:72        sdk = (e["sdk"]["python"] or "—").split("(")[0]73        w(f"| `{e['method']}` | `{e['path']}` | {e['name']} | {params_for(e)} | `{sdk}` | {st(e)} |")7475w("""76## 3. Roles & scopes catalogue7778| Level | Predefined roles (legacy strings) | Where they appear |79|---|---|---|80| Organization | `owner` (billing, members, all reader rights), `reader` (make API requests, view org info, manage resources) | `Invite.role`, `OrganizationUser.role`, `POST /organization/users/{id}` body `role` |81| Project | `owner`, `member` | `ProjectUser.role`, `Invite.projects[].role`, `POST /organization/projects/{id}/users` |82| Custom roles | `Role{id, name, description, permissions[], resource_type, predefined_role}` created with `POST /organization/roles` (org-wide) or `POST /projects/{id}/roles`; assigned with the `…/roles` binding endpoints (`role_id`) | Roles API |83| API key permission levels (dashboard) | *All*, *Restricted* (per-resource read/write), *Read-only*; restricted keys carry scopes like `api.model.request`; audit log `api_key.created.data.scopes[]` records them | dashboard + audit logs |84| Key types | project key `sk-proj-…` (owned by a user or a **service account**, `owner.type: user|service_account`, `owner_project_access: active|inactive`), legacy user key, service-account key (`POST /organization/projects/{p}/service_accounts` returns `api_key.value` once, or `POST …/service_accounts/{id}/api_keys` to mint another with optional `expires_at`), Admin key `sk-admin-…`, WIF short-lived bearer | see `docs/openai/authentication-and-keys.md` |8586Governance knobs documented in production-best-practices: max API-key lifetime (org and project, project ≤ org), *API Key Governance* (allow only service-account keys / only user keys / block new keys; org restrictions win), key usage tracking (keys created before 2023-12-20 are untracked by default).8788## 4. Usage & costs query cookbook (`ACCOUNT_RESTRICTED` for us — verified 403 `api.usage.read`)8990All usage endpoints share the same query grammar (from the OpenAPI spec):9192| Param | Type | Notes |93|---|---|---|94| `start_time`* | int (unix s) | inclusive |95| `end_time` | int | exclusive |96| `bucket_width` | `1m` / `1h` / `1d` (default `1d`) | costs: only `1d` |97| `limit` | int | buckets per page: `1d` ≤ 31 (default 7), `1h` ≤ 168 (default 24), `1m` ≤ 1440 (default 60) |98| `page` | string | cursor from `next_page` |99| `project_ids`, `user_ids`, `api_key_ids`, `models` | string[] | filters (repeat the query key) |100| `batch` | bool | completions/embeddings/…: true = Batch API traffic only |101| `group_by` | string[] | completions: `project_id, user_id, api_key_id, model, batch, service_tier`; costs: `project_id, line_item`; images add `size, source`; audio speeches/transcriptions add `model`; code_interpreter_sessions/vector_stores: `project_id` |102103| Endpoint | Result object | Key metrics |104|---|---|---|105| `GET /v1/organization/usage/completions` | `organization.usage.completions.result` | `input_tokens, input_cached_tokens, input_cache_write_tokens, input_uncached_tokens, output_tokens, input/output_text|audio|image_tokens, num_model_requests`, dims `project_id, user_id, api_key_id, model, batch, service_tier` |106| `…/usage/embeddings` | `organization.usage.embeddings.result` | `input_tokens, num_model_requests` |107| `…/usage/moderations` | `organization.usage.moderations.result` | `input_tokens, num_model_requests` |108| `…/usage/images` | `organization.usage.images.result` | `images, num_model_requests`, dims `size` (`256x256`…`1536x1024`), `source` (`image.generation, image.edit, image.variation`) |109| `…/usage/audio_speeches` | `organization.usage.audio_speeches.result` | `characters, num_model_requests` |110| `…/usage/audio_transcriptions` | `organization.usage.audio_transcriptions.result` | `seconds, num_model_requests` |111| `…/usage/vector_stores` | `organization.usage.vector_stores.result` | `usage_bytes` |112| `…/usage/code_interpreter_sessions` | `organization.usage.code_interpreter_sessions.result` | `num_sessions` |113| `…/usage/file_search_calls`, `…/usage/web_search_calls` | `organization.usage.*_calls.result` | `num_calls` (web search adds `search_context_size`) |114| `GET /v1/organization/costs` | `organization.costs.result` | `amount{value, currency:"usd"}, line_item, project_id, organization_id` |115116Recipes (curl; `-G` turns `--data-urlencode` into query params):117118```bash119# daily completions tokens per model for the last 7 days120curl -G https://api.openai.com/v1/organization/usage/completions -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \\121  --data-urlencode "start_time=$(( $(date +%s) - 7*86400 ))" --data-urlencode bucket_width=1d \\122  --data-urlencode group_by=model --data-urlencode group_by=service_tier --data-urlencode limit=7123# hourly usage of one project, Batch only, paginated124curl -G https://api.openai.com/v1/organization/usage/completions -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \\125  --data-urlencode start_time=1789000000 --data-urlencode bucket_width=1h --data-urlencode project_ids=proj_XXX \\126  --data-urlencode batch=true --data-urlencode limit=168 --data-urlencode page=page_AAAA…127# spend in USD per day per line item and project128curl -G https://api.openai.com/v1/organization/costs -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \\129  --data-urlencode start_time=$(( $(date +%s) - 30*86400 )) --data-urlencode bucket_width=1d \\130  --data-urlencode group_by=line_item --data-urlencode group_by=project_id --data-urlencode limit=31131```132133Python: `client.admin.organization.usage.completions(start_time=…, bucket_width="1d", group_by=["model"], limit=7)`; costs: `client.admin.organization.usage.costs(...)`. Runnable examples: `examples/openai/admin/usage_costs.sh`, `admin_readonly.py`, `admin_readonly.ts` (all return 403 on our account).134135## 5. Spend limits, alerts, data retention, permissions136137| Resource | Endpoints | Body / semantics |138|---|---|---|139| Org spend limit | `GET/POST/DELETE /v1/organization/spend_limit` | `threshold_amount` (cents), `currency: USD`, `interval: month`; hitting it → `429 organization_spend_limit_exceeded` on API traffic |140| Project spend limit | `GET/POST/DELETE /v1/organization/projects/{p}/spend_limit` | same body; → `429 project_spend_limit_exceeded` |141| Spend alerts | `/v1/organization/spend_alerts[/{id}]`, `/v1/organization/projects/{p}/spend_alerts[/{id}]` | `threshold_amount`, `currency`, `interval`, `notification_channel{type:"email", recipients[], subject_prefix}` |142| Data retention | `GET/POST /v1/organization/data_retention`, `GET/POST /v1/organization/projects/{p}/data_retention` | project `retention_type: organization_default` inherits; org sets the policy (ZDR / modified retention need approval) |143| Model permissions | `GET/POST/DELETE /v1/organization/projects/{p}/model_permissions` | `mode: allow_list | deny_list`, `model_ids[]` (must be visible to the org, fine-tuned snapshots allowed); denied models return `404 model_not_found` |144| Hosted tool permissions | `GET/POST /v1/organization/projects/{p}/hosted_tool_permissions` | allow/deny hosted tools (web search, code interpreter, file search, MCP, …) per project |145| Rate limits | `GET /v1/organization/projects/{p}/rate_limits`, `POST …/rate_limits/{rate_limit_id}` | `project.rate_limit{model, max_requests_per_1_minute, max_tokens_per_1_minute, max_images_per_1_minute, max_audio_megabytes_per_1_minute, max_requests_per_1_day, batch_1_day_max_input_tokens}`; a project limit can only lower the org limit |146| Certificates (mTLS) | org: `GET/POST /v1/organization/certificates`, `…/{id}` GET/POST/DELETE, `…/activate`, `…/deactivate`; project: `GET …/projects/{p}/certificates`, `…/activate`, `…/deactivate` | upload PEM trust anchor (`content`, `name`), activate = enforce; API mTLS host `mtls.api.openai.com`; scopes `api.mtls.read/write` |147| External storage | `GET/POST /v1/organization/external_storage`, `GET/DELETE …/{id}`, `POST …/{id}/validate` | bring-your-own bucket for stored artifacts; audit events `external_storage.registered/removed` |148| Archive project | `POST /v1/organization/projects/{p}/archive` | irreversible; `status: archived` |149150## 6. Audit logs151152`GET /v1/organization/audit_logs` (scope `api.audit_logs.read`; **HTTP 401** not 403 without it). Filters: `effective_at[gt|gte|lt|lte]`, `project_ids[]`, `event_types[]`, `actor_ids[]`, `actor_emails[]`, `resource_ids[]`, `limit`, `after`, `before`. Each entry: `{object:"organization.audit_log", id, type, effective_at, project{id,name}, actor{type: session|api_key, session{user, ip_address, user_agent, ja3, ja4, ip_address_details}, api_key{type: user|service_account, id, user|service_account}}, <type>: {payload}}`.153154""")155cats = OrderedDict()156for a in A:157    cats.setdefault(a["category"], []).append(a["event"])158w("| Category | Event types (149 total) |")159w("|---|---|")160for c, evs in cats.items():161    w(f"| {c} | " + ", ".join(f"`{e}`" for e in evs) + " |")162w("\nPayload fields per event are in `generated/fragments/audit-log-events/openai-audit-log-events.json` (57 events carry a typed payload in the spec; the `tenant.*` enterprise-workspace events are enum-only).\n")163w("""164## 7. Verified behaviour with a non-admin key (2026-09-18)165166| Call | HTTP | Body |167|---|---|---|168| `GET /v1/organization/{users,invites,projects,admin_api_keys,spend_limit,spend_alerts,data_retention}` | 403 | `{"error": "You have insufficient permissions for this operation. Missing scopes: api.management.read. Check that you have the correct role in your organization, and if you're using a restricted API key, that it has the necessary scopes."}` — note `error` is a **bare string** |169| `GET /v1/organization/usage/completions`, `GET /v1/organization/costs` | 403 | same shape, `api.usage.read` |170| `GET /v1/organization/roles` / `groups` | 403 | `api.roles.read` / `api.groups.read` |171| `GET /v1/organization/certificates` | 403 | `api.mtls.read … correct role in your organization (Owner)` |172| `GET /v1/organization/external_storage` | 403 | structured: `{"error":{"message":"… api.external_storage.read …","type":"invalid_request_error","param":null,"code":"insufficient_permissions"}}` |173| `GET /v1/organization/audit_logs` | **401** | structured, `api.audit_logs.read`, `code: null` |174175Response headers on these errors still include `x-request-id`, `openai-version: 2020-10-01`, `openai-processing-ms`, and (except certificates/audit logs) `openai-organization` / `openai-project`.176177## 8. SDK access178179```python180from openai import OpenAI181client = OpenAI(admin_api_key=os.environ["OPENAI_ADMIN_KEY"])      # Python >= 2.34.0182users = client.admin.organization.users.list(limit=20)              # SyncConversationCursorPage -> iterate183for u in users: print(u.id, u.email, u.role)184client.admin.organization.projects.model_permissions.update("proj_abc", mode="allow_list", model_ids=["gpt-4.1"])185```186```ts187const client = new OpenAI({ adminAPIKey: process.env.OPENAI_ADMIN_KEY }); // Node >= 6.36.0188for await (const log of client.admin.organization.auditLogs.list({ limit: 50 })) console.log(log.type);189```190Go `option.WithAdminAPIKey` (≥ 3.34.0), Java `OpenAIOkHttpClient.builder().adminApiKey(...)` (≥ 4.34.0), Ruby `OpenAI::Client.new(admin_api_key:)` (≥ 0.61.0).191""")192(ROOT / "docs/openai/admin-api.md").write_text("\n".join(lines))193print("wrote docs/openai/admin-api.md", len("\n".join(lines)), "chars;", len(E), "endpoints in", len(groups), "groups")194