@acedatacloud/skills 2026.628.4 → 2026.628.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md
CHANGED
|
@@ -31,7 +31,7 @@ Skills are located in the `skills/` directory (also mirrored to `.agents/skills/
|
|
|
31
31
|
- **face-transform** — Face analysis, beautification, age/gender transform, swap, cartoon
|
|
32
32
|
- **short-url** — Create and manage short URLs
|
|
33
33
|
- **onepage-pdf** — Convert an HTML page into one tall single-page PDF (local; no API token; needs Python + pymupdf + Chrome/Edge)
|
|
34
|
-
- **acedatacloud
|
|
34
|
+
- **acedatacloud** — Manage your AceDataCloud account — balance, usage/spend, API keys, services, orders, announcements
|
|
35
35
|
|
|
36
36
|
## Authentication
|
|
37
37
|
|
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ Compatible with **30+ AI coding agents** via the [agentskills.io](https://agents
|
|
|
50
50
|
| [face-transform](skills/face-transform/) | Face analysis, beautification, age/gender transform, swap, cartoon |
|
|
51
51
|
| [short-url](skills/short-url/) | Create and manage short URLs |
|
|
52
52
|
| [onepage-pdf](skills/onepage-pdf/) | Convert an HTML page into one tall single-page PDF — no pagination breaks (local, no token) |
|
|
53
|
-
| [acedatacloud
|
|
53
|
+
| [acedatacloud](skills/acedatacloud/) | Manage your AceDataCloud account — balance, usage/spend, API keys, services, orders, announcements |
|
|
54
54
|
|
|
55
55
|
### Connectors
|
|
56
56
|
|
|
@@ -207,6 +207,7 @@ Skills provide **knowledge** (when to use, parameters, gotchas). MCP servers pro
|
|
|
207
207
|
| nano-banana-image | [mcp-nano-banana](https://pypi.org/project/mcp-nano-banana/) | `pip install mcp-nano-banana` | `https://nano-banana.mcp.acedata.cloud/mcp` |
|
|
208
208
|
| short-url | [mcp-shorturl](https://pypi.org/project/mcp-shorturl/) | `pip install mcp-shorturl` | `https://short-url.mcp.acedata.cloud/mcp` |
|
|
209
209
|
| wan-video | [mcp-wan](https://pypi.org/project/mcp-wan/) | `pip install mcp-wan` | `https://wan.mcp.acedata.cloud/mcp` |
|
|
210
|
+
| acedatacloud | [mcp-acedatacloud](https://pypi.org/project/mcp-acedatacloud/) | `pip install mcp-acedatacloud` | `https://acedatacloud.mcp.acedata.cloud/mcp` |
|
|
210
211
|
|
|
211
212
|
**Using hosted MCP endpoints** (no local install needed):
|
|
212
213
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acedatacloud/skills",
|
|
3
|
-
"version": "2026.628.
|
|
3
|
+
"version": "2026.628.5",
|
|
4
4
|
"description": "Agent Skills for AceDataCloud AI services — music, image, video generation, LLM chat, web search. Compatible with Claude Code, GitHub Copilot, Gemini CLI, OpenAI Codex, and 30+ AI coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skills",
|
|
@@ -18,6 +18,7 @@ Each AceDataCloud service has a corresponding MCP server that provides tool-use
|
|
|
18
18
|
| nano-banana-image | `pip install mcp-nano-banana` | `https://nano-banana.mcp.acedata.cloud/mcp` |
|
|
19
19
|
| short-url | `pip install mcp-shorturl` | `https://short-url.mcp.acedata.cloud/mcp` |
|
|
20
20
|
| wan-video | `pip install mcp-wan` | `https://wan.mcp.acedata.cloud/mcp` |
|
|
21
|
+
| acedatacloud | `pip install mcp-acedatacloud` | `https://acedatacloud.mcp.acedata.cloud/mcp` |
|
|
21
22
|
|
|
22
23
|
## Configuration Example
|
|
23
24
|
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: acedatacloud
|
|
3
|
+
description: |
|
|
4
|
+
Manage your AceDataCloud account through the management API
|
|
5
|
+
(platform.acedata.cloud). Use when the user wants to check their balance /
|
|
6
|
+
remaining credits, look up API call (usage) records and spend, list or create
|
|
7
|
+
or delete API keys (credentials), list subscribed services, list/create/pay
|
|
8
|
+
recharge orders, manage platform tokens, list available models, or (admins)
|
|
9
|
+
publish an announcement. This is the self-service "console" API — distinct
|
|
10
|
+
from the data-generation APIs (image/video/music/search).
|
|
11
|
+
license: Apache-2.0
|
|
12
|
+
metadata:
|
|
13
|
+
author: acedatacloud
|
|
14
|
+
version: "1.0"
|
|
15
|
+
connections: [acedatacloud]
|
|
16
|
+
compatibility: Requires ACEDATACLOUD_PLATFORM_TOKEN (a platform or user token). Auto-injected when the AceDataCloud connector is installed; otherwise set it in .env. Optionally pair with mcp-acedatacloud for tool-use.
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# AceDataCloud Platform Management
|
|
20
|
+
|
|
21
|
+
Programmatically manage your AceDataCloud account: balances, usage records, API
|
|
22
|
+
keys, services, orders, platform tokens, models, and announcements.
|
|
23
|
+
|
|
24
|
+
This is the **management / console** API at `https://platform.acedata.cloud/api/v1`
|
|
25
|
+
— the same surface the web console uses. It is **different** from the
|
|
26
|
+
data-generation API at `api.acedata.cloud` (image / video / music / search generation).
|
|
27
|
+
|
|
28
|
+
## Setup — use a PLATFORM token, not a service token
|
|
29
|
+
|
|
30
|
+
The management API is authenticated with a **platform token** (or a logged-in
|
|
31
|
+
user token), **not** the per-service API token used for `api.acedata.cloud`.
|
|
32
|
+
|
|
33
|
+
1. Create one at [platform.acedata.cloud/console/platform-tokens](https://platform.acedata.cloud/console/platform-tokens)
|
|
34
|
+
(or `POST /api/v1/platform-tokens/`). It starts with `platform-` and never expires.
|
|
35
|
+
2. Provide it one of two ways:
|
|
36
|
+
- **Connector (recommended in studio / chat):** install the **AceDataCloud**
|
|
37
|
+
connector at [auth.acedata.cloud/user/connections](https://auth.acedata.cloud/user/connections)
|
|
38
|
+
and paste the token once — the runtime injects `ACEDATACLOUD_PLATFORM_TOKEN`
|
|
39
|
+
into the sandbox automatically (this skill declares `connections: [acedatacloud]`).
|
|
40
|
+
- **Local `.env`:**
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
ACEDATACLOUD_PLATFORM_TOKEN=platform-v1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
curl -H "authorization: Bearer $ACEDATACLOUD_PLATFORM_TOKEN" \
|
|
48
|
+
https://platform.acedata.cloud/api/v1/applications/
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> A normal token only ever sees **its own** account data. A superuser token sees
|
|
52
|
+
> every user's data and is required for admin operations (announcements).
|
|
53
|
+
|
|
54
|
+
## CLI (preferred)
|
|
55
|
+
|
|
56
|
+
The skill ships [`scripts/acedatacloud.py`](scripts/acedatacloud.py) — a self-contained
|
|
57
|
+
CLI (stdlib only) for the most common operations.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
ADC=$SKILL_DIR/scripts/acedatacloud.py
|
|
61
|
+
|
|
62
|
+
# Read
|
|
63
|
+
python3 $ADC balance # remaining credits per subscription
|
|
64
|
+
python3 $ADC services --search suno # list/search subscribed services
|
|
65
|
+
python3 $ADC usage --days 7 # recent API call records
|
|
66
|
+
python3 $ADC usage-summary --days 30 # spend aggregated by day + API
|
|
67
|
+
python3 $ADC keys # list API keys (credentials)
|
|
68
|
+
python3 $ADC orders --state Finished # recharge orders
|
|
69
|
+
python3 $ADC tokens # platform tokens
|
|
70
|
+
python3 $ADC models # available chat models
|
|
71
|
+
|
|
72
|
+
# Safe write (require --yes to actually execute)
|
|
73
|
+
python3 $ADC create-key --application <app-id> --name "ci" --yes
|
|
74
|
+
python3 $ADC delete-key --id <credential-id> --yes
|
|
75
|
+
python3 $ADC create-order --application <app-id> --package <package-id> --yes
|
|
76
|
+
python3 $ADC pay-order --id <order-id> --pay-way Stripe --yes
|
|
77
|
+
python3 $ADC create-token --yes
|
|
78
|
+
python3 $ADC delete-token --id <token-id> --yes
|
|
79
|
+
|
|
80
|
+
# Admin (superuser token only)
|
|
81
|
+
python3 $ADC send-announcement --title "..." --content "..." --yes
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Every command accepts `--json` for machine-readable output and `--token` to
|
|
85
|
+
override the env token. Without `--yes`, write commands print a dry-run preview
|
|
86
|
+
and exit without calling the API.
|
|
87
|
+
|
|
88
|
+
## Pagination
|
|
89
|
+
|
|
90
|
+
List endpoints return `{ "count": <total>, "items": [ ... ] }` and accept
|
|
91
|
+
`?limit=` and `?offset=`. (Note: some older docs say `results`; the live field
|
|
92
|
+
is **`items`**.)
|
|
93
|
+
|
|
94
|
+
## Endpoints reference
|
|
95
|
+
|
|
96
|
+
### Balance / subscriptions — `GET /applications/`
|
|
97
|
+
|
|
98
|
+
An *Application* is your subscription to one Service; its `remaining_amount` is
|
|
99
|
+
your balance (in **Credits**) for that service.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
curl -H "authorization: Bearer $ACEDATACLOUD_PLATFORM_TOKEN" \
|
|
103
|
+
"https://platform.acedata.cloud/api/v1/applications/?limit=1"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"count": 3,
|
|
109
|
+
"items": [
|
|
110
|
+
{
|
|
111
|
+
"id": "e9f625f2-cbd5-4254-8264-cfadbd180428",
|
|
112
|
+
"service_id": "f2b646d8-3cfd-46ef-969a-1ea9eebde329",
|
|
113
|
+
"remaining_amount": 100.9,
|
|
114
|
+
"used_amount": 1.1,
|
|
115
|
+
"paid": false,
|
|
116
|
+
"scope": "Global",
|
|
117
|
+
"allow_consume_global": true,
|
|
118
|
+
"expired_at": null,
|
|
119
|
+
"type": "Usage"
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Filters: `service_id`, `scope` (`Individual`/`Global`), `user_id` (superuser only).
|
|
126
|
+
|
|
127
|
+
### Usage records — `GET /usage/apis/`
|
|
128
|
+
|
|
129
|
+
Per-request call records (status code, latency, credits deducted).
|
|
130
|
+
|
|
131
|
+
Filters: `api_id`, `application_id`, `status_code`, `created_at_from`,
|
|
132
|
+
`created_at_to`, `user_id` (superuser only).
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"count": 128,
|
|
137
|
+
"items": [
|
|
138
|
+
{
|
|
139
|
+
"id": "c124684b-7188-4c3e-ad53-3fba5150b944",
|
|
140
|
+
"api": { "title": "Suno Audios Generation API" },
|
|
141
|
+
"status_code": 200,
|
|
142
|
+
"original_amount": 0.55,
|
|
143
|
+
"deducted_amount": 0.55,
|
|
144
|
+
"trace_id": "…",
|
|
145
|
+
"elapsed": 12.3,
|
|
146
|
+
"created_at": "2026-06-28T09:15:44Z"
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- **Spend aggregate** — `GET /usage/apis/aggregate/?created_at_from=&created_at_to=`
|
|
153
|
+
→ `{ "items": [{ "date", "api_id", "amount" }], "total": <credits>, "apis": { "<id>": { "title" } } }`
|
|
154
|
+
- **Status-code filter values** — `GET /usage/apis/status-codes/` → `{ "items": [200, 400, 502] }`
|
|
155
|
+
- **CSV export** — `GET /usage/apis/export/?created_at_from=&created_at_to=` (downloads `usages.csv`)
|
|
156
|
+
|
|
157
|
+
### Services — `GET /services/`
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{ "count": 215, "items": [ { "id": "…", "alias": "suno", "title": "Suno 音乐生成",
|
|
161
|
+
"unit": "Credit", "free_amount": 0.0, "applied_count": 121733,
|
|
162
|
+
"packages": [ { "id": "…", "type": "Usage", "price": 13.0, "amount": 100.0 } ] } ] }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### API keys / credentials — `GET /credentials/`
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{ "count": 2, "items": [ { "id": "dae4899f-…", "name": "ci", "type": "Token",
|
|
169
|
+
"token": "<secret>", "limited_amount": null, "used_amount": 1.1,
|
|
170
|
+
"host": null, "created_at": "2026-06-28T09:15:44Z" } ] }
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
- **Create** — `POST /credentials/` `{ "application_id": "<app>", "name": "ci", "limited_amount": 100, "expired_at": "2026-12-31T00:00:00Z" }` → returns the credential incl. the new `token`. Save it; it is shown in full only at creation.
|
|
174
|
+
- **Delete** — `DELETE /credentials/{id}` → `204 No Content`
|
|
175
|
+
- Filters: `application_id`, `host`, `granted` (`true`/`false`), `user_id` (superuser).
|
|
176
|
+
- To "rotate" a key, delete it and create a new one.
|
|
177
|
+
|
|
178
|
+
### Orders (recharge) — `GET /orders/`
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{ "count": 12, "items": [ { "id": "17c10c9d-…", "application_id": "…",
|
|
182
|
+
"package_id": "…", "description": "100 Credits", "price": 13.0,
|
|
183
|
+
"state": "Finished", "pay_way": "Stripe", "pay_url": "<url>",
|
|
184
|
+
"created_at": "2026-06-28T09:17:25Z" } ] }
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
- `state` ∈ `Pending` `Paid` `Finished` `Expired` `Failed` `Refunded`
|
|
188
|
+
- `pay_way` ∈ `WechatPay` `AliPay` `Stripe` `X402` `PayPal` `Reward`
|
|
189
|
+
- **Create** — `POST /orders/` `{ "application_id": "<app>", "package_id": "<package>" }`
|
|
190
|
+
- **Pay** — `POST /orders/{id}/pay/` `{ "pay_way": "Stripe" }` → returns the order with a `pay_url` to open
|
|
191
|
+
- **Refresh status** — `POST /orders/{id}/refresh/` (re-checks the PSP and updates `state`)
|
|
192
|
+
- Filters: `state`, `pay_way`, `created_at_from`, `created_at_to`, `user_id` (superuser).
|
|
193
|
+
|
|
194
|
+
### Platform tokens — `GET /platform-tokens/`
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{ "count": 2, "items": [ { "id": "efdccba4-…", "token": "<secret>",
|
|
198
|
+
"expiration": null, "used_at": null, "created_at": "2026-06-28T06:29:02Z" } ] }
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
- **Create** — `POST /platform-tokens/` → returns the new token (starts with `platform-`)
|
|
202
|
+
- **Delete** — `DELETE /platform-tokens/{id}/` → `204 No Content`
|
|
203
|
+
|
|
204
|
+
### Models — `GET /models/`
|
|
205
|
+
|
|
206
|
+
OpenAI-style (no pagination): `{ "object": "list", "data": [ { "id": "gpt-4.1",
|
|
207
|
+
"label": "GPT-4.1", "owned_by": "openai", "type": "chat", "capabilities": ["vision"] } ] }`
|
|
208
|
+
|
|
209
|
+
### Announcements — `GET /announcements/`
|
|
210
|
+
|
|
211
|
+
Public read: `{ "count": 20, "items": [ { "id", "title", "content",
|
|
212
|
+
"translation_key", "published", "publish_at", "rank", "tags", "is_read" } ] }`
|
|
213
|
+
|
|
214
|
+
**Admin only (superuser token):**
|
|
215
|
+
|
|
216
|
+
- **Publish** — `POST /announcements/admin/` `{ "title": "...", "content": "...", "rank": 5, "tags": ["product"], "published": true }`
|
|
217
|
+
(`content` is Markdown; a zh-cn Translation row is created and other locales are auto-translated by a CronJob)
|
|
218
|
+
- **Edit / delete** — `PUT` / `DELETE /announcements/admin/{id}`
|
|
219
|
+
- **AI polish** — `POST /announcements/admin/polish/` `{ "title", "content" }`
|
|
220
|
+
- **AI translate** — `POST /announcements/admin/translate/` `{ "translation_key" }`
|
|
221
|
+
|
|
222
|
+
## Write-operation safety
|
|
223
|
+
|
|
224
|
+
Creating/deleting keys, creating/paying orders, deleting platform tokens, and
|
|
225
|
+
publishing announcements are **irreversible or money-related**. Always:
|
|
226
|
+
|
|
227
|
+
1. Confirm the exact target (`application_id`, `order_id`, `credential_id`) with
|
|
228
|
+
the user before executing.
|
|
229
|
+
2. With the CLI, writes are dry-run unless `--yes` is passed.
|
|
230
|
+
3. Never print a full `token` value into shared logs — it grants account access.
|
|
231
|
+
|
|
232
|
+
## Gotchas
|
|
233
|
+
|
|
234
|
+
- Use the **platform token** (`ACEDATACLOUD_PLATFORM_TOKEN`), not the
|
|
235
|
+
`api.acedata.cloud` service token — the latter returns 401 here.
|
|
236
|
+
- `remaining_amount` / `used_amount` / `amount` are in **Credits**, not USD.
|
|
237
|
+
Convert with a service's package: `USD = Credits × (package.price / package.amount)`.
|
|
238
|
+
- Newly created credential/platform tokens are returned in full **only once** —
|
|
239
|
+
store them immediately.
|
|
240
|
+
- Credential "rotate" is delete + recreate; there is no in-place rotate endpoint.
|
|
241
|
+
- Announcement endpoints under `/admin/` require a **superuser** token; a normal
|
|
242
|
+
token gets `403`.
|
|
243
|
+
|
|
244
|
+
> **MCP:** `pip install mcp-acedatacloud` | See [all MCP servers](../_shared/mcp-servers.md). The MCP exposes these as tools (`platform_get_balance`, `platform_list_usage`, `platform_create_credential`, `platform_create_announcement`, …) with the same write-confirmation guard.
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""AceDataCloud platform management CLI (self-contained, stdlib only).
|
|
3
|
+
|
|
4
|
+
Talks to the management API at https://platform.acedata.cloud/api/v1 using a
|
|
5
|
+
platform token (ACEDATACLOUD_PLATFORM_TOKEN). Read commands are always safe;
|
|
6
|
+
write commands are dry-run unless --yes is passed.
|
|
7
|
+
|
|
8
|
+
Examples:
|
|
9
|
+
python3 platform.py balance
|
|
10
|
+
python3 platform.py usage-summary --days 30
|
|
11
|
+
python3 platform.py keys
|
|
12
|
+
python3 platform.py create-key --application <app-id> --name ci --yes
|
|
13
|
+
python3 platform.py send-announcement --title T --content C --yes
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import datetime as dt
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import urllib.error
|
|
23
|
+
import urllib.parse
|
|
24
|
+
import urllib.request
|
|
25
|
+
|
|
26
|
+
DEFAULT_BASE_URL = os.environ.get("PLATFORM_API_BASE_URL", "https://platform.acedata.cloud")
|
|
27
|
+
SECRET_KEYS = {"token", "password", "pay_url", "pay_id"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _die(msg: str, code: int = 1) -> None:
|
|
31
|
+
print(f"error: {msg}", file=sys.stderr)
|
|
32
|
+
sys.exit(code)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _token(args: argparse.Namespace) -> str:
|
|
36
|
+
token = args.token or os.environ.get("ACEDATACLOUD_PLATFORM_TOKEN", "")
|
|
37
|
+
if not token:
|
|
38
|
+
_die(
|
|
39
|
+
"no platform token. Set ACEDATACLOUD_PLATFORM_TOKEN or pass --token.\n"
|
|
40
|
+
"Create one at https://platform.acedata.cloud/console/platform-tokens"
|
|
41
|
+
)
|
|
42
|
+
return token
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _request(args: argparse.Namespace, method: str, path: str,
|
|
46
|
+
params: dict | None = None, body: dict | None = None) -> tuple[int, object]:
|
|
47
|
+
base = args.base_url.rstrip("/")
|
|
48
|
+
url = f"{base}/api/v1{path}"
|
|
49
|
+
if params:
|
|
50
|
+
clean = {k: v for k, v in params.items() if v is not None}
|
|
51
|
+
if clean:
|
|
52
|
+
url += "?" + urllib.parse.urlencode(clean, doseq=True)
|
|
53
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
54
|
+
req = urllib.request.Request(url, data=data, method=method, headers={
|
|
55
|
+
"accept": "application/json",
|
|
56
|
+
"authorization": f"Bearer {_token(args)}",
|
|
57
|
+
"content-type": "application/json",
|
|
58
|
+
})
|
|
59
|
+
try:
|
|
60
|
+
with urllib.request.urlopen(req, timeout=args.timeout) as resp:
|
|
61
|
+
raw = resp.read().decode("utf-8")
|
|
62
|
+
return resp.status, (json.loads(raw) if raw.strip() else None)
|
|
63
|
+
except urllib.error.HTTPError as e:
|
|
64
|
+
raw = e.read().decode("utf-8", "replace")
|
|
65
|
+
try:
|
|
66
|
+
return e.code, json.loads(raw)
|
|
67
|
+
except Exception:
|
|
68
|
+
return e.code, raw
|
|
69
|
+
except urllib.error.URLError as e:
|
|
70
|
+
_die(f"network error: {e.reason}")
|
|
71
|
+
return 0, None # unreachable
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _get_all(args: argparse.Namespace, path: str, params: dict | None = None,
|
|
75
|
+
page: int = 200, cap: int = 2000) -> tuple[int, list]:
|
|
76
|
+
"""Fetch every page of a {count, items} list endpoint (capped)."""
|
|
77
|
+
base = dict(params or {})
|
|
78
|
+
out: list = []
|
|
79
|
+
offset = 0
|
|
80
|
+
total = 0
|
|
81
|
+
while len(out) < cap:
|
|
82
|
+
base.update({"limit": page, "offset": offset})
|
|
83
|
+
status, payload = _request(args, "GET", path, base)
|
|
84
|
+
_check_ok(status, payload)
|
|
85
|
+
items = payload.get("items", []) if isinstance(payload, dict) else []
|
|
86
|
+
total = payload.get("count", 0) if isinstance(payload, dict) else 0
|
|
87
|
+
out.extend(items)
|
|
88
|
+
offset += page
|
|
89
|
+
if not items or offset >= total:
|
|
90
|
+
break
|
|
91
|
+
return total, out
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _mask(obj: object, reveal: bool) -> object:
|
|
95
|
+
if reveal:
|
|
96
|
+
return obj
|
|
97
|
+
if isinstance(obj, dict):
|
|
98
|
+
out = {}
|
|
99
|
+
for k, v in obj.items():
|
|
100
|
+
if k in SECRET_KEYS and isinstance(v, str) and v:
|
|
101
|
+
out[k] = v[:10] + "…" + f"({len(v)} chars)" if len(v) > 10 else "***"
|
|
102
|
+
else:
|
|
103
|
+
out[k] = _mask(v, reveal)
|
|
104
|
+
return out
|
|
105
|
+
if isinstance(obj, list):
|
|
106
|
+
return [_mask(x, reveal) for x in obj]
|
|
107
|
+
return obj
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _emit(args: argparse.Namespace, payload: object, rows: list[tuple] | None = None,
|
|
111
|
+
headers: tuple | None = None, force_reveal: bool = False) -> None:
|
|
112
|
+
reveal = force_reveal or getattr(args, "reveal", False)
|
|
113
|
+
if args.json:
|
|
114
|
+
print(json.dumps(_mask(payload, reveal), ensure_ascii=False, indent=2))
|
|
115
|
+
return
|
|
116
|
+
if rows is not None and headers is not None:
|
|
117
|
+
widths = [len(h) for h in headers]
|
|
118
|
+
for r in rows:
|
|
119
|
+
for i, cell in enumerate(r):
|
|
120
|
+
widths[i] = max(widths[i], len(str(cell)))
|
|
121
|
+
line = " ".join(str(h).ljust(widths[i]) for i, h in enumerate(headers))
|
|
122
|
+
print(line)
|
|
123
|
+
print(" ".join("-" * widths[i] for i in range(len(headers))))
|
|
124
|
+
for r in rows:
|
|
125
|
+
print(" ".join(str(c).ljust(widths[i]) for i, c in enumerate(r)))
|
|
126
|
+
else:
|
|
127
|
+
print(json.dumps(_mask(payload, reveal), ensure_ascii=False, indent=2))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _check_ok(status: int, payload: object) -> None:
|
|
131
|
+
if status >= 400:
|
|
132
|
+
msg = payload
|
|
133
|
+
if isinstance(payload, dict):
|
|
134
|
+
err = payload.get("error")
|
|
135
|
+
msg = (err.get("message") if isinstance(err, dict) else err) or payload.get("detail") or payload
|
|
136
|
+
_die(f"HTTP {status}: {msg}", code=2)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _since(days: int | None) -> str | None:
|
|
140
|
+
if not days:
|
|
141
|
+
return None
|
|
142
|
+
return (dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---- read commands -------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
def cmd_balance(args):
|
|
148
|
+
status, payload = _request(args, "GET", "/applications/", {"limit": args.limit})
|
|
149
|
+
_check_ok(status, payload)
|
|
150
|
+
items = payload.get("items", []) if isinstance(payload, dict) else []
|
|
151
|
+
rows = [(
|
|
152
|
+
it.get("service_id", "")[:8],
|
|
153
|
+
it.get("remaining_amount"),
|
|
154
|
+
it.get("used_amount"),
|
|
155
|
+
it.get("scope"),
|
|
156
|
+
it.get("id", "")[:8],
|
|
157
|
+
) for it in items]
|
|
158
|
+
_emit(args, payload, rows, ("service", "remaining", "used", "scope", "app_id"))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def cmd_services(args):
|
|
162
|
+
if args.search:
|
|
163
|
+
total, items = _get_all(args, "/services/")
|
|
164
|
+
s = args.search.lower()
|
|
165
|
+
items = [it for it in items if s in (it.get("alias") or "").lower() or s in (it.get("title") or "").lower()]
|
|
166
|
+
payload = {"count": len(items), "items": items}
|
|
167
|
+
if not args.json:
|
|
168
|
+
print(f"# {len(items)} match '{args.search}' / {total} total services\n")
|
|
169
|
+
else:
|
|
170
|
+
status, payload = _request(args, "GET", "/services/", {"limit": args.limit})
|
|
171
|
+
_check_ok(status, payload)
|
|
172
|
+
items = payload.get("items", []) if isinstance(payload, dict) else []
|
|
173
|
+
if not args.json:
|
|
174
|
+
print(f"# {len(items)} shown / {payload.get('count')} total services\n")
|
|
175
|
+
rows = [(it.get("alias"), (it.get("title") or "")[:28], it.get("unit"), it.get("applied_count"), it.get("id", "")[:8]) for it in items]
|
|
176
|
+
_emit(args, payload, rows, ("alias", "title", "unit", "applied", "id"))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def cmd_usage(args):
|
|
180
|
+
params = {"limit": args.limit, "status_code": args.status_code,
|
|
181
|
+
"api_id": args.api, "created_at_from": _since(args.days)}
|
|
182
|
+
status, payload = _request(args, "GET", "/usage/apis/", params)
|
|
183
|
+
_check_ok(status, payload)
|
|
184
|
+
items = payload.get("items", []) if isinstance(payload, dict) else []
|
|
185
|
+
rows = [(
|
|
186
|
+
((it.get("api") or {}).get("title") or "")[:30],
|
|
187
|
+
it.get("status_code"),
|
|
188
|
+
it.get("deducted_amount"),
|
|
189
|
+
it.get("elapsed"),
|
|
190
|
+
(it.get("created_at") or "")[:19],
|
|
191
|
+
) for it in items]
|
|
192
|
+
_emit(args, payload, rows, ("api", "status", "credits", "elapsed", "created_at"))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def cmd_usage_summary(args):
|
|
196
|
+
params = {"created_at_from": _since(args.days or 30), "api_id": args.api}
|
|
197
|
+
status, payload = _request(args, "GET", "/usage/apis/aggregate/", params)
|
|
198
|
+
_check_ok(status, payload)
|
|
199
|
+
apis = payload.get("apis", {}) if isinstance(payload, dict) else {}
|
|
200
|
+
by_api: dict[str, float] = {}
|
|
201
|
+
for it in (payload.get("items", []) if isinstance(payload, dict) else []):
|
|
202
|
+
by_api[it.get("api_id")] = by_api.get(it.get("api_id"), 0.0) + (it.get("amount") or 0.0)
|
|
203
|
+
rows = sorted(((apis.get(k, {}).get("title", k or "?")[:36], round(v, 4)) for k, v in by_api.items()),
|
|
204
|
+
key=lambda r: -r[1])
|
|
205
|
+
if not args.json:
|
|
206
|
+
print(f"# total spend: {payload.get('total')} Credits over last {args.days or 30} days\n")
|
|
207
|
+
_emit(args, payload, rows, ("api", "credits"))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def cmd_keys(args):
|
|
211
|
+
status, payload = _request(args, "GET", "/credentials/", {"limit": args.limit, "application_id": args.application})
|
|
212
|
+
_check_ok(status, payload)
|
|
213
|
+
items = payload.get("items", []) if isinstance(payload, dict) else []
|
|
214
|
+
rows = [(
|
|
215
|
+
it.get("id", "")[:8],
|
|
216
|
+
it.get("name") or "-",
|
|
217
|
+
it.get("type"),
|
|
218
|
+
it.get("limited_amount"),
|
|
219
|
+
it.get("used_amount"),
|
|
220
|
+
(it.get("created_at") or "")[:10],
|
|
221
|
+
) for it in items]
|
|
222
|
+
_emit(args, payload, rows, ("id", "name", "type", "limit", "used", "created"))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def cmd_orders(args):
|
|
226
|
+
status, payload = _request(args, "GET", "/orders/", {"limit": args.limit, "state": args.state, "pay_way": args.pay_way})
|
|
227
|
+
_check_ok(status, payload)
|
|
228
|
+
items = payload.get("items", []) if isinstance(payload, dict) else []
|
|
229
|
+
rows = [(
|
|
230
|
+
it.get("id", "")[:8],
|
|
231
|
+
(it.get("description") or "")[:26],
|
|
232
|
+
it.get("price"),
|
|
233
|
+
it.get("state"),
|
|
234
|
+
it.get("pay_way"),
|
|
235
|
+
(it.get("created_at") or "")[:10],
|
|
236
|
+
) for it in items]
|
|
237
|
+
_emit(args, payload, rows, ("id", "description", "price", "state", "pay_way", "created"))
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def cmd_tokens(args):
|
|
241
|
+
status, payload = _request(args, "GET", "/platform-tokens/", {"limit": args.limit})
|
|
242
|
+
_check_ok(status, payload)
|
|
243
|
+
items = payload.get("items", []) if isinstance(payload, dict) else []
|
|
244
|
+
rows = [(it.get("id", "")[:8], (it.get("used_at") or "never")[:19], (it.get("created_at") or "")[:19]) for it in items]
|
|
245
|
+
_emit(args, payload, rows, ("id", "last_used", "created"))
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def cmd_models(args):
|
|
249
|
+
status, payload = _request(args, "GET", "/models/")
|
|
250
|
+
_check_ok(status, payload)
|
|
251
|
+
data = payload.get("data", []) if isinstance(payload, dict) else []
|
|
252
|
+
rows = [(m.get("id"), (m.get("label") or "")[:24], m.get("owned_by"), m.get("type"), ",".join(m.get("capabilities") or [])) for m in data]
|
|
253
|
+
_emit(args, payload, rows, ("id", "label", "owned_by", "type", "capabilities"))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def cmd_announcements(args):
|
|
257
|
+
status, payload = _request(args, "GET", "/announcements/", {"limit": args.limit})
|
|
258
|
+
_check_ok(status, payload)
|
|
259
|
+
items = payload.get("items", []) if isinstance(payload, dict) else []
|
|
260
|
+
rows = [(it.get("id", "")[:8], (it.get("title") or "")[:40], it.get("rank"), (it.get("publish_at") or "")[:10]) for it in items]
|
|
261
|
+
_emit(args, payload, rows, ("id", "title", "rank", "publish_at"))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ---- write commands (gated by --yes) -------------------------------------
|
|
265
|
+
|
|
266
|
+
def _guard(args, action: str, detail: dict) -> bool:
|
|
267
|
+
if args.yes:
|
|
268
|
+
return True
|
|
269
|
+
print("DRY RUN — pass --yes to execute.")
|
|
270
|
+
print(f"action: {action}")
|
|
271
|
+
print(f"target: {json.dumps(detail, ensure_ascii=False)}")
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def cmd_create_key(args):
|
|
276
|
+
body = {"application_id": args.application}
|
|
277
|
+
if args.name:
|
|
278
|
+
body["name"] = args.name
|
|
279
|
+
if args.limited_amount is not None:
|
|
280
|
+
body["limited_amount"] = args.limited_amount
|
|
281
|
+
if args.expired_at:
|
|
282
|
+
body["expired_at"] = args.expired_at
|
|
283
|
+
if not _guard(args, "POST /credentials/", body):
|
|
284
|
+
return
|
|
285
|
+
status, payload = _request(args, "POST", "/credentials/", body=body)
|
|
286
|
+
_check_ok(status, payload)
|
|
287
|
+
print("# new key created — store the token now, it is shown in full only once")
|
|
288
|
+
_emit(args, payload, force_reveal=True)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def cmd_delete_key(args):
|
|
292
|
+
if not _guard(args, f"DELETE /credentials/{args.id}", {"id": args.id}):
|
|
293
|
+
return
|
|
294
|
+
status, payload = _request(args, "DELETE", f"/credentials/{args.id}")
|
|
295
|
+
_check_ok(status, payload)
|
|
296
|
+
print(f"deleted credential {args.id} (HTTP {status})")
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def cmd_create_order(args):
|
|
300
|
+
body = {"application_id": args.application, "package_id": args.package}
|
|
301
|
+
if not _guard(args, "POST /orders/", body):
|
|
302
|
+
return
|
|
303
|
+
status, payload = _request(args, "POST", "/orders/", body=body)
|
|
304
|
+
_check_ok(status, payload)
|
|
305
|
+
_emit(args, payload)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def cmd_pay_order(args):
|
|
309
|
+
body = {"pay_way": args.pay_way}
|
|
310
|
+
if not _guard(args, f"POST /orders/{args.id}/pay/", body):
|
|
311
|
+
return
|
|
312
|
+
status, payload = _request(args, "POST", f"/orders/{args.id}/pay/", body=body)
|
|
313
|
+
_check_ok(status, payload)
|
|
314
|
+
if isinstance(payload, dict) and payload.get("pay_url"):
|
|
315
|
+
print(f"pay_url: {payload['pay_url']}")
|
|
316
|
+
_emit(args, payload)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def cmd_create_token(args):
|
|
320
|
+
if not _guard(args, "POST /platform-tokens/", {}):
|
|
321
|
+
return
|
|
322
|
+
status, payload = _request(args, "POST", "/platform-tokens/", body={})
|
|
323
|
+
_check_ok(status, payload)
|
|
324
|
+
print("# new platform token — store it now, it is shown in full only once")
|
|
325
|
+
_emit(args, payload, force_reveal=True)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def cmd_delete_token(args):
|
|
329
|
+
if not _guard(args, f"DELETE /platform-tokens/{args.id}/", {"id": args.id}):
|
|
330
|
+
return
|
|
331
|
+
status, payload = _request(args, "DELETE", f"/platform-tokens/{args.id}/")
|
|
332
|
+
_check_ok(status, payload)
|
|
333
|
+
print(f"deleted platform token {args.id} (HTTP {status})")
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def cmd_send_announcement(args):
|
|
337
|
+
body = {"title": args.title, "content": args.content, "published": not args.draft}
|
|
338
|
+
if args.rank is not None:
|
|
339
|
+
body["rank"] = args.rank
|
|
340
|
+
if args.tags:
|
|
341
|
+
body["tags"] = args.tags.split(",")
|
|
342
|
+
if not _guard(args, "POST /announcements/admin/ (superuser only)", body):
|
|
343
|
+
return
|
|
344
|
+
status, payload = _request(args, "POST", "/announcements/admin/", body=body)
|
|
345
|
+
_check_ok(status, payload)
|
|
346
|
+
_emit(args, payload)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
350
|
+
# Global options use SUPPRESS defaults so a value parsed BEFORE the
|
|
351
|
+
# subcommand is not clobbered by the subparser re-injecting its own
|
|
352
|
+
# default. Without SUPPRESS, argparse overwrites `--json foo` back to
|
|
353
|
+
# False. Real defaults are applied in main() via _GLOBAL_DEFAULTS, so the
|
|
354
|
+
# options work in both positions (e.g. `--json keys` and `keys --json`).
|
|
355
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
356
|
+
common.add_argument("--token", default=argparse.SUPPRESS,
|
|
357
|
+
help="platform token (default: $ACEDATACLOUD_PLATFORM_TOKEN)")
|
|
358
|
+
common.add_argument("--base-url", default=argparse.SUPPRESS, help="API base URL")
|
|
359
|
+
common.add_argument("--timeout", type=float, default=argparse.SUPPRESS,
|
|
360
|
+
help="request timeout in seconds (default: 30)")
|
|
361
|
+
common.add_argument("--json", action="store_true", default=argparse.SUPPRESS,
|
|
362
|
+
help="raw JSON output")
|
|
363
|
+
common.add_argument("--reveal", action="store_true", default=argparse.SUPPRESS,
|
|
364
|
+
help="do not mask secrets in output")
|
|
365
|
+
|
|
366
|
+
p = argparse.ArgumentParser(description="AceDataCloud platform management CLI", parents=[common])
|
|
367
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
368
|
+
|
|
369
|
+
def add(name, fn, help_):
|
|
370
|
+
sp = sub.add_parser(name, help=help_, parents=[common])
|
|
371
|
+
sp.set_defaults(func=fn)
|
|
372
|
+
return sp
|
|
373
|
+
|
|
374
|
+
sp = add("balance", cmd_balance, "remaining credits per subscription")
|
|
375
|
+
sp.add_argument("--limit", type=int, default=50)
|
|
376
|
+
|
|
377
|
+
sp = add("services", cmd_services, "list/search subscribed services")
|
|
378
|
+
sp.add_argument("--limit", type=int, default=100)
|
|
379
|
+
sp.add_argument("--search", help="filter by alias/title substring")
|
|
380
|
+
|
|
381
|
+
sp = add("usage", cmd_usage, "recent API call records")
|
|
382
|
+
sp.add_argument("--limit", type=int, default=20)
|
|
383
|
+
sp.add_argument("--days", type=int, help="only records newer than N days")
|
|
384
|
+
sp.add_argument("--status-code", type=int)
|
|
385
|
+
sp.add_argument("--api", help="filter by api_id")
|
|
386
|
+
|
|
387
|
+
sp = add("usage-summary", cmd_usage_summary, "spend aggregated by API")
|
|
388
|
+
sp.add_argument("--days", type=int, default=30)
|
|
389
|
+
sp.add_argument("--api", help="filter by api_id")
|
|
390
|
+
|
|
391
|
+
sp = add("keys", cmd_keys, "list API keys (credentials)")
|
|
392
|
+
sp.add_argument("--limit", type=int, default=50)
|
|
393
|
+
sp.add_argument("--application", help="filter by application_id")
|
|
394
|
+
|
|
395
|
+
sp = add("orders", cmd_orders, "list recharge orders")
|
|
396
|
+
sp.add_argument("--limit", type=int, default=20)
|
|
397
|
+
sp.add_argument("--state")
|
|
398
|
+
sp.add_argument("--pay-way", dest="pay_way")
|
|
399
|
+
|
|
400
|
+
sp = add("tokens", cmd_tokens, "list platform tokens")
|
|
401
|
+
sp.add_argument("--limit", type=int, default=50)
|
|
402
|
+
|
|
403
|
+
add("models", cmd_models, "list available chat models")
|
|
404
|
+
|
|
405
|
+
sp = add("announcements", cmd_announcements, "list announcements")
|
|
406
|
+
sp.add_argument("--limit", type=int, default=20)
|
|
407
|
+
|
|
408
|
+
sp = add("create-key", cmd_create_key, "create an API key (needs --yes)")
|
|
409
|
+
sp.add_argument("--application", required=True)
|
|
410
|
+
sp.add_argument("--name")
|
|
411
|
+
sp.add_argument("--limited-amount", dest="limited_amount", type=float)
|
|
412
|
+
sp.add_argument("--expired-at", dest="expired_at")
|
|
413
|
+
sp.add_argument("--yes", action="store_true")
|
|
414
|
+
|
|
415
|
+
sp = add("delete-key", cmd_delete_key, "delete an API key (needs --yes)")
|
|
416
|
+
sp.add_argument("--id", required=True)
|
|
417
|
+
sp.add_argument("--yes", action="store_true")
|
|
418
|
+
|
|
419
|
+
sp = add("create-order", cmd_create_order, "create a recharge order (needs --yes)")
|
|
420
|
+
sp.add_argument("--application", required=True)
|
|
421
|
+
sp.add_argument("--package", required=True)
|
|
422
|
+
sp.add_argument("--yes", action="store_true")
|
|
423
|
+
|
|
424
|
+
sp = add("pay-order", cmd_pay_order, "create a pay link for an order (needs --yes)")
|
|
425
|
+
sp.add_argument("--id", required=True)
|
|
426
|
+
sp.add_argument("--pay-way", dest="pay_way", default="Stripe")
|
|
427
|
+
sp.add_argument("--yes", action="store_true")
|
|
428
|
+
|
|
429
|
+
sp = add("create-token", cmd_create_token, "create a platform token (needs --yes)")
|
|
430
|
+
sp.add_argument("--yes", action="store_true")
|
|
431
|
+
|
|
432
|
+
sp = add("delete-token", cmd_delete_token, "delete a platform token (needs --yes)")
|
|
433
|
+
sp.add_argument("--id", required=True)
|
|
434
|
+
sp.add_argument("--yes", action="store_true")
|
|
435
|
+
|
|
436
|
+
sp = add("send-announcement", cmd_send_announcement, "publish an announcement (superuser; needs --yes)")
|
|
437
|
+
sp.add_argument("--title", required=True)
|
|
438
|
+
sp.add_argument("--content", required=True)
|
|
439
|
+
sp.add_argument("--rank", type=int)
|
|
440
|
+
sp.add_argument("--tags", help="comma-separated")
|
|
441
|
+
sp.add_argument("--draft", action="store_true", help="create unpublished")
|
|
442
|
+
sp.add_argument("--yes", action="store_true")
|
|
443
|
+
|
|
444
|
+
return p
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
_GLOBAL_DEFAULTS = {
|
|
448
|
+
"token": None,
|
|
449
|
+
"base_url": DEFAULT_BASE_URL,
|
|
450
|
+
"timeout": 30.0,
|
|
451
|
+
"json": False,
|
|
452
|
+
"reveal": False,
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def main() -> None:
|
|
457
|
+
args = build_parser().parse_args()
|
|
458
|
+
# Apply defaults for any global option not supplied in either position
|
|
459
|
+
# (they use argparse.SUPPRESS so unset attrs are simply absent).
|
|
460
|
+
for attr, default in _GLOBAL_DEFAULTS.items():
|
|
461
|
+
if not hasattr(args, attr):
|
|
462
|
+
setattr(args, attr, default)
|
|
463
|
+
args.func(args)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
if __name__ == "__main__":
|
|
467
|
+
main()
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: acedatacloud-api
|
|
3
|
-
description: Guide for using AceDataCloud APIs. Use when authenticating, making API calls, managing credentials, understanding billing, or integrating AceDataCloud services into applications. Covers setup, authentication, request patterns, error handling, and SDK integration.
|
|
4
|
-
license: Apache-2.0
|
|
5
|
-
metadata:
|
|
6
|
-
author: acedatacloud
|
|
7
|
-
version: "1.0"
|
|
8
|
-
compatibility: Requires an AceDataCloud account at platform.acedata.cloud.
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# AceDataCloud API Usage Guide
|
|
12
|
-
|
|
13
|
-
Complete guide for using AceDataCloud's AI-powered data services API.
|
|
14
|
-
|
|
15
|
-
## Getting Started
|
|
16
|
-
|
|
17
|
-
### 1. Create an Account
|
|
18
|
-
|
|
19
|
-
Register at [platform.acedata.cloud](https://platform.acedata.cloud).
|
|
20
|
-
|
|
21
|
-
### 2. Subscribe to a Service
|
|
22
|
-
|
|
23
|
-
Browse available services and click **Get** to subscribe. Most services include free quota.
|
|
24
|
-
|
|
25
|
-
### 3. Create API Credentials
|
|
26
|
-
|
|
27
|
-
Go to your service's **Credentials** page and create an API Token.
|
|
28
|
-
|
|
29
|
-
> **Full details:** See [authentication](../_shared/authentication.md) for token types and usage.
|
|
30
|
-
|
|
31
|
-
## SDK Integration (OpenAI-Compatible)
|
|
32
|
-
|
|
33
|
-
For chat completion services, use the standard OpenAI SDK:
|
|
34
|
-
|
|
35
|
-
```python
|
|
36
|
-
from openai import OpenAI
|
|
37
|
-
|
|
38
|
-
client = OpenAI(
|
|
39
|
-
api_key="YOUR_API_TOKEN",
|
|
40
|
-
base_url="https://api.acedata.cloud/v1"
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
response = client.chat.completions.create(
|
|
44
|
-
model="claude-sonnet-4-20250514",
|
|
45
|
-
messages=[{"role": "user", "content": "Hello!"}]
|
|
46
|
-
)
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
```javascript
|
|
50
|
-
import OpenAI from "openai";
|
|
51
|
-
|
|
52
|
-
const client = new OpenAI({
|
|
53
|
-
apiKey: "YOUR_API_TOKEN",
|
|
54
|
-
baseURL: "https://api.acedata.cloud/v1"
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const response = await client.chat.completions.create({
|
|
58
|
-
model: "gpt-4.1",
|
|
59
|
-
messages: [{ role: "user", content: "Hello!" }]
|
|
60
|
-
});
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## Request Patterns
|
|
64
|
-
|
|
65
|
-
### Synchronous APIs
|
|
66
|
-
|
|
67
|
-
Some APIs return results immediately (e.g., face transform, search):
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
curl -X POST https://api.acedata.cloud/face/analyze \
|
|
71
|
-
-H "Authorization: Bearer $TOKEN" \
|
|
72
|
-
-H "Content-Type: application/json" \
|
|
73
|
-
-d '{"image_url": "https://example.com/photo.jpg"}'
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
### Async Task APIs
|
|
77
|
-
|
|
78
|
-
Most generation APIs (images, video, music) are asynchronous.
|
|
79
|
-
|
|
80
|
-
> **Full details:** See [async task polling](../_shared/async-tasks.md) for the submit-and-poll pattern.
|
|
81
|
-
|
|
82
|
-
## Error Handling
|
|
83
|
-
|
|
84
|
-
| HTTP Code | Meaning | Action |
|
|
85
|
-
|-----------|---------|--------|
|
|
86
|
-
| 400 | Bad request | Check request parameters |
|
|
87
|
-
| 401 | Unauthorized | Check API token |
|
|
88
|
-
| 403 | Forbidden | Content filtered or insufficient permissions |
|
|
89
|
-
| 429 | Rate limited | Wait and retry with backoff |
|
|
90
|
-
| 500 | Server error | Retry or contact support |
|
|
91
|
-
|
|
92
|
-
Error response format:
|
|
93
|
-
|
|
94
|
-
```json
|
|
95
|
-
{
|
|
96
|
-
"error": {
|
|
97
|
-
"code": "token_mismatched",
|
|
98
|
-
"message": "Invalid or expired token"
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
## Billing
|
|
104
|
-
|
|
105
|
-
- Each API call deducts from your **subscription balance** (remaining_amount)
|
|
106
|
-
- Cost varies by service, model, and usage (tokens, requests, data size)
|
|
107
|
-
- Check balance at [platform.acedata.cloud](https://platform.acedata.cloud)
|
|
108
|
-
- Most services offer free trial quota
|
|
109
|
-
|
|
110
|
-
## Service Categories
|
|
111
|
-
|
|
112
|
-
| Category | Services | Base Path |
|
|
113
|
-
|----------|----------|-----------|
|
|
114
|
-
| **AI Chat** | GPT, Claude, Gemini, Kimi, Grok | `/v1/chat/completions` |
|
|
115
|
-
| **Image Gen** | Flux, Seedream, NanoBanana | `/flux/*`, `/seedream/*`, etc. |
|
|
116
|
-
| **Video Gen** | Luma, Sora, Veo, Kling, Hailuo, Seedance, Wan | `/luma/*`, `/sora/*`, etc. |
|
|
117
|
-
| **Music Gen** | Suno, Producer, Fish Audio | `/suno/*`, `/producer/*`, `/fish/*` |
|
|
118
|
-
| **Search** | Google Search (web/images/news/maps) | `/serp/*` |
|
|
119
|
-
| **Face** | Analyze, beautify, swap, cartoon, age | `/face/*` |
|
|
120
|
-
| **Utility** | Short URL, QR Art, Headshots | `/short-url`, `/qrart/*`, `/headshots/*` |
|
|
121
|
-
|
|
122
|
-
## Gotchas
|
|
123
|
-
|
|
124
|
-
- Tokens are **service-scoped** by default — create a global token if you need cross-service access
|
|
125
|
-
- Async APIs return a `task_id` — always use `callback_url` to get the task_id immediately, then poll for results
|
|
126
|
-
- Avoid `wait: true` — it blocks for the full generation duration and will time out for video/music tasks
|
|
127
|
-
- Rate limits vary by service tier — upgrade your plan if hitting limits
|
|
128
|
-
- All timestamps are in UTC
|
|
129
|
-
|
|
130
|
-
> **MCP:** See [MCP servers](../_shared/mcp-servers.md) for tool-use integration with AI agents.
|