# 调用日志 API

> 用同一把 sk-gpushare-* Key 拉取逐笔调用记录 —— 状态、报错原因、异步任务终态、实扣积分,可增量同步进自己的系统

来源：https://qianduan.dflop.top/docs/reference/logs-api

[logs.dflop.top](https://logs.dflop.top) 上那本逐笔账,现在可以用**同一把 API Key** 直接拉。典型用途:

- 把调用流水**同步进自己的系统**做对账 / 报表 / 告警
- 查**某一笔为什么失败**(错误码、脱敏后的错误原文、网关内部换过几次道)
- 查**异步任务的终态**(视频 / 音乐 / 语音 / 出图):提交时拿到的 `id` 粘进 `ref` 就能反查

> 只读接口,**不消耗余额**。余额为 0 的 Key 照常可以读日志 —— 那正是最需要查原因的时候。

| 端点 | 用途 |
|---|---|
| `GET /v1/logs` | 逐笔调用,筛选 + 翻页 |
| `GET /v1/logs/{id}` | 单笔的技术视图(请求参数 / 上游回包 / 网关) |
| `GET /v1/logs/summary` | 时段聚合(按天 / 按模型 / 四种终态计数) |

Base URL `https://qianyi.dflop.top`,鉴权与聊天端点同一套(`Authorization: Bearer sk-gpushare-…` 或 `x-api-key`),见[鉴权](./authentication.md)。

---

## 可见范围(先读这一段)

**默认只能看到这把 Key 自己打出去的调用。** 这是有意的:一把分发出去的 Key 不该能列举同账号其他 Key 的流水。

```bash
# 默认：只有这把 Key 的记录
curl "https://qianyi.dflop.top/v1/logs?limit=5" \
  -H "Authorization: Bearer $PLATFORM_API_KEY"
```

要一次拉走**账号下全部 Key** 的流水,需要账号主人在控制台逐把 Key 显式打开:

> [qianduan.dflop.top → API Keys](https://qianduan.dflop.top/dashboard/keys) → 点进某把 Key → 勾选「允许这把 Key 读取账号全部调用日志」

打开后:

```bash
curl "https://qianyi.dflop.top/v1/logs?scope=account&limit=5" \
  -H "Authorization: Bearer $PLATFORM_API_KEY"

# 账号级下还可以按某一把 Key 收窄
curl "https://qianyi.dflop.top/v1/logs?scope=account&key_id=<uuid>" \
  -H "Authorization: Bearer $PLATFORM_API_KEY"
```

没开就传 `scope=account` → **403** `permission_denied`。

| 情况 | `scope=key`(默认) | `scope=account` |
|---|---|---|
| 自己在控制台创建的 Key | ✅ | 需勾选开关 |
| 企业签发给员工的 Key | ✅ | ❌ 恒不可用 |
| 算力包 / 桌面端凭据 | ✅ | ❌ 恒不可用 |
| 智能体 Key(`/v1/agent/*` 专用) | ❌ 403 | ❌ 403 |

---

## GET /v1/logs

### 查询参数

| 参数 | 默认 | 说明 |
|---|---|---|
| `scope` | `key` | `key` = 只看这把 Key;`account` = 账号下全部(需开关) |
| `key_id` | — | 仅 `scope=account`:收窄到某一把 Key |
| `period` | `30d` | `today` / `7d` / `30d` / `this_month` / `all`。给了 `from`+`to` 时忽略 |
| `from` / `to` | — | **必须成对给**。`YYYY-MM-DD` 或 RFC3339 时间戳,两种形态可混用 |
| `model` | — | 精确 model id,**逗号多选**:`model=gpt-5.5,glm-4.7` |
| `status` | — | **逗号多选**:`success` / `error` / `interrupted` / `rejected` |
| `unit_type` | — | **逗号多选**:`image` / `video` / `music` / `audio` / `avatar` / `voice` / `search` / `transcript`,外加 `chat`(= 按 token 计费的对话行) |
| `error_code` | — | 精确错误码,**逗号多选**。码表见[错误码](./errors.md) |
| `ref` | — | 按**任务 ID 或请求 ID** 精确反查(两列都试,不用分辨手里那串是哪一种) |
| `order` | `desc` | `desc` 最新在前;`asc` 最旧在前(增量拉取用) |
| `limit` | `50` | 1–200 |
| `cursor` | — | 上一页响应里的 `next_cursor`,原样回传 |
| `include` | `attempts` | 逗号多选:`attempts`(换道明细)/ `in_flight`(未完成的异步任务)/ `none` |

> **`to` 的右端语义随形态而变**,这一点务必注意:
> - `to=2026-01-03`(日期形态)→ **包含 1 月 3 日一整天**
> - `to=2026-01-03T12:00:00Z`(时间戳形态)→ **不含该时刻本身**(`created_at < to`)
>
> 增量同步请用时间戳形态 —— 上一次的 `to` 原样当下一次的 `from`,不重不漏。

> **筛选参数为空串 = 不筛**,不是「筛掉一切」。`?status=` 与不传 `status` 等价。
> 状态值写错(如 `?status=failed`)会得到 **400**,不会静默返回空页。

### 响应

```jsonc
{
  "object": "list",
  "currency": "points",          // 所有金额字段的单位:积分
  "scope": "key",                // 本次实际生效的可见范围
  "period_from": "2026-10-08T00:00:00Z",
  "period_to":   "2026-11-07T03:21:00Z",
  "data": [
    {
      "object": "log",
      "id": 90210,
      "created_at":   "2026-11-07T03:18:42Z",  // 终态时刻(异步任务 = 结算时刻)
      "submitted_at": "2026-11-07T03:17:05Z",  // 请求进入网关的时刻
      "key_id": "…", "key_name": "prod", "key_prefix": "sk-gpushare-a1b2",
      "model": "doubao-seedance-2.0",
      "status": "success",                      // success|error|interrupted|rejected
      "error_code": null,
      "error_message": null,
      "input_tokens": 0, "output_tokens": 0, "cached_tokens": 0,
      "unit_count": 5, "unit_type": "video",    // 按件计费行;null = 按 token 计费
      "cost": "182.40",                         // 实扣积分,JSON **字符串**
      "latency_ms": 97210,
      "request_id": "3f9c…",                    // = 响应头 x-gateway-trace
      "task_id": "9b1e…",                       // 异步任务提交响应里的那个 id
      "result_urls": ["https://…"],
      "result_expires_at": "2026-11-08T03:18:42Z",
      "attempts_count": 1,                      // null = 这一轮没查（见下）
      "attempts": [                             // include=attempts 时才有
        { "seq": 1, "created_at": "…", "latency_ms": 4100,
          "error_code": "upstream_error", "error_message": "上游暂时不可用" }
      ]
    }
  ],
  "has_more": true,
  "next_cursor": "d:1762..._90210_1759..._1762..."
}
```

**金额是 JSON 字符串**(`"182.40"`),不是数字 —— 刻意如此,不让钱经过 IEEE-754。解析时按十进制处理。

### 一行 = 一次对外调用的终态

- 网关在多条上游线路间**自动换道**产生的失败尝试**不单独成行、不计费、不计入请求数**,它们折叠在对应终态行的 `attempts[]` 里(`attempts_count` 是条数)。
  ⚠️ `attempts_count` 是 **`null`** 表示「这一轮没查到」—— 不带 `include=attempts`,或那次子查询自己失败了。**不是 0**。回 0 会把「我们没看」说成「确实没换过道」,而这个字段的用途恰恰是判断「这笔有没有抖过」。
- 四种状态:`success` 成功 / `error` 失败 / `interrupted` 中断(客户端断开或网关超时)/ `rejected` **已拒绝**(提交那一刻被网关拒:参数错误、模型不可用、余额不足、内容拦截,**费用恒为 0**,但会入账,便于核对「发了但没成」)。
- **异步任务要跑到终态结算后才成为账目行。** 还在生成中的任务用 `include=in_flight` 单独拿(默认不返回 —— 它比列表本身贵得多)。⚠️ 它**只在第一页给**(没有 `cursor` 的那次请求):翻页时一份就够了。翻页响应里该字段**整个缺席**,与「要了但确实没有在飞任务」(空数组)在 wire 上分得开。

### 翻页

```bash
# 第一页
curl "https://qianyi.dflop.top/v1/logs?period=7d&limit=100" -H "Authorization: Bearer $KEY"
# 拿 next_cursor 继续,直到 has_more 为 false
curl "https://qianyi.dflop.top/v1/logs?cursor=<next_cursor>" -H "Authorization: Bearer $KEY"
```

游标是不透明串,**原样回传**。它冻住了第一页解析出的时间窗 —— 所以翻页途中 `period=7d` 不会随时间向前滑走、把最老的一小片漏掉。

⚠️ 游标里记了方向:拿 `order=desc` 的游标去请求 `order=asc` 会得到 **400**,不会静默给你一段错位的数据。

---

## 增量同步配方

把逐笔流水持续拉进自己的库,推荐这个形状:

```bash
# 水位线 = 上一次成功同步到的时刻（RFC3339）
WATERMARK="2026-11-07T03:00:00Z"
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)

curl -sS "https://qianyi.dflop.top/v1/logs?from=${WATERMARK}&to=${NOW}&order=asc&limit=200" \
  -H "Authorization: Bearer $PLATFORM_API_KEY"
# 翻完全部页后，把 WATERMARK 推进到 NOW
```

**两条必须注意的:**

1. **按 `id` 去重。** 行的写入顺序在高并发下可能与 `created_at` 有极小的错位(数据库序列的正常行为),所以这不是严格意义上的变更流。把水位线**往回退 5 分钟**重叠拉一段、按 `id` 去重,就能覆盖这种错位。`id` 全局唯一且不复用。
2. **异步任务按终态时刻(`created_at`)入账,不是提交时刻。** 一个跑了 20 分钟的视频任务,会出现在它**结算那一刻**的时间窗里。想按提交时刻对齐,用行上的 `submitted_at`。

---

## GET /v1/logs/{id}

单笔的**技术视图** —— 这一单到底是什么分辨率、什么时长、上游报回来什么。`{id}` 是列表行里的 `id`。

```bash
curl "https://qianyi.dflop.top/v1/logs/90210" -H "Authorization: Bearer $KEY"
```

```jsonc
{
  "id": 90210,
  "kind": "video",                 // video|image|music|audio|null(对话/检索)
  "request":  [{ "k": "resolution", "v": "1080p" }, { "k": "duration", "v": "5" }],
  "upstream": [{ "k": "framespersecond", "v": "24" }, { "k": "billable_tokens", "v": "…" }],
  "gateway":  [{ "k": "client_protocol", "v": "openai_chat" }, { "k": "request_bytes", "v": "…" }],
  "request_note": null             // 参数为空时的原因（如任务结算后清空了快照）
}
```

- 字段名**刻意不翻译**:它们就是 API 文档里的原名,方便逐条对照。
- 三组都可能是空数组 —— 对话轮没有任务行,老任务没有上游快照。空 ≠ 错误。
- 可见范围与列表一致:没开账号级开关的 Key 去取别的 Key 那一行,得到 **404**。

---

## GET /v1/logs/summary

时段聚合,用来对账。

```bash
curl "https://qianyi.dflop.top/v1/logs/summary?period=this_month" -H "Authorization: Bearer $KEY"
```

```jsonc
{
  "object": "logs.summary",
  "currency": "points",
  "scope": "key",
  "period_from": "…", "period_to": "…",
  "total": { "requests": 1840, "cost": "9123.55",
             "input_tokens": 8812340, "output_tokens": 412990, "cached_tokens": 6610220 },
  "status_counts": { "success": 1802, "error": 21, "interrupted": 4, "rejected": 13 },
  "by_day":   [{ "date": "2026-11-01T00:00:00Z", "requests": 61, "cost": "302.10", "input_tokens": 0, "output_tokens": 0, "cached_tokens": 0 }],
  "by_model": [{ "model": "gpt-5.5", "requests": 1204, "cost": "5120.00", "total_tokens": 7712330 }]
}
```

与列表面的差异,三条:

- **不吃行级筛选**。`model` / `status` / `unit_type` / `error_code` / `ref` 传了会返回 **400** —— 它是区间内的**全量**聚合,静默忽略这些参数会让你按 `?model=X` 拿回整个账号的数字然后记在 X 头上。要按模型看,直接读响应里的 `by_model[]`;要行级筛选用 `/v1/logs`。
- **时段上限 365 天**,且**不接受 `period=all`** —— 不封顶会拖垮共享资源。要更长的历史请分段拉。
- **结果缓存 60 秒**(按整分钟对齐的时间桶)。刚落账的那一笔最多晚一分钟出现在这里;要实时对账用 `/v1/logs`(逐笔面没有这层缓存)。

`status_counts` 不含内部换道尝试;成功率的分母通常取 `success + error + interrupted`(`rejected` 的请求根本没跑到上游)。

---

## 限流

| | |
|---|---|
| 默认 | **每把 Key 每分钟 60 次**(三个端点共用这一档) |
| 与推理配额的关系 | **完全分开**。狂拉日志不会让你的业务请求撞 429 |
| 响应头 | `x-ratelimit-limit` / `x-ratelimit-remaining` / `x-ratelimit-reset`(秒) |
| 超限 | **429** `logs_rate_limited` + `Retry-After` |

对账场景本来就不需要高频:每分钟拉一次、每次 200 条,足够跟上任何量级。

---

## 错误

| 状态 | `code` | 含义 |
|---|---|---|
| 400 | `invalid_request` | 参数不合法(状态值写错 / 半截区间 / 游标方向不匹配 / `include` 里有不认识的 token / 给 `/summary` 传了行级筛选) |
| 401 | `invalid_api_key` | Key 不存在、已停用、已过期,或账号非活跃 |
| 403 | `permission_denied` | 未开启账号级开关就用 `scope=account`;或用智能体 Key 调本接口 |
| 404 | `log_not_found` | 该 `id` 不存在,或不在这把 Key 的可见范围内 |
| 429 | `logs_rate_limited` | 超过每分钟上限,按 `Retry-After` 重试 |

错误体是 OpenAI 形状:`{"error": {"message", "type", "code"}}`。完整说明见[错误码](./errors.md)。

---

## 与网页版 / CSV 的字段对照

[logs.dflop.top](https://logs.dflop.top) 的 CSV 导出与本接口是同一份数据,只有两处命名不同:

| CSV / 网页 | 本接口 | 说明 |
|---|---|---|
| `cost_usd` | `cost` | **两者都是积分**。CSV 那个列名是全平台去美元化之前的历史遗留,已有客户脚本按名字取值所以不改;新接口不继承它 |
| `logs[]` | `data[]` | 换成 OpenAI 的列表形状(`object` + `data` + `has_more`) |

其余字段逐字同名同义。CSV 里的 `attempts_count` 对应本接口的同名字段,`result_urls` 在 CSV 里以 `|` 分隔、在这里是数组。

---

## 脱敏说明

`error_message` / `error_code` 在离开服务端前会统一脱敏:URL、密钥串、上游线路名称一律遮蔽。这不是截断错误信息 —— 分类(`error_code`)与可读原因都保留,只是不暴露我们的上游拓扑。排障时把 `request_id` 一起提供即可。
