# YunQi AI 完整 API Reference（供 Agent 读取）

> 文档版本：v1.0.11
> 更新日期：2026-09-14  
> 内容：API 协议、模型参数、文件输入、客户端配置与可运行示例，包含 GPT Image 2.5 Flare / Sunburst 和 MJ / 悠船。

本文件提供 YunQi AI 的接口、参数、媒体格式、图片尺寸、返回结构、错误处理和客户端配置。它适合直接交给 Codex、Claude Code、WorkBuddy 或其他开发 Agent，用来完成接入、实现调用、排查错误和解析返回。

Agent 直读入口为文档站同域名的 `/llms-full.txt`，索引为 `/llms.txt`。这些地址不需要登录或 API Key；它们属于文档站，不是 API 服务端点。

## 0. Agent 执行规则

1. 让用户在 YunQi AI 控制台创建 API Key，并将密钥保存在环境变量或客户端私有配置中。
2. 开始实现前调用 `GET /v1/models`，以这把 API Key 实际返回的完整模型 ID 为准。若返回 401/403，先停止后续生成调用，处理密钥或分组权限；不要用更换模型、端点或参数绕过。
3. 根据模型和任务选择协议，不要混用不同协议的字段结构。
4. 先发送只含必填字段的最小请求，再加入流式输出、工具、多模态或图片参数。
5. 遍历完整响应数组和内容块，不要只读取第一个图片或第一个内容块。
6. 不要把 API Key 写进仓库、日志、截图或回复正文。
7. 用户决定模型；不要自行替换型号。按对应模型章节构造请求，不跨型号套用参数或接口。
8. GPT Image 2.5 编辑使用 `/v1/images/edits?api-version=2025-04-01-preview`，不发送 `response_format`、`input_fidelity`、`output_format=webp`。完整字段见 12A。
9. MJ 使用 `POST /v1/tob/diffusion` 的 `text` 字段，再用 `GET /v1/tob/job/{jobId}` 查询。`subscribe` 是上游账户查询路径，不能用于生图。完整异步流程见 12B。

10. 多轮会话由客户端维护历史。Responses 回传原始 `input`、完整 `output[]`（含 reasoning 项）和工具结果，不使用 `previous_response_id` 或响应检索接口保存会话。
11. POST 超时或连接断开时结果可能未知；不要自动重放、切换型号或重复执行有副作用的工具。SDK 示例关闭内置自动重试。

## 1. 连接清单

```yaml
provider: YunQi AI
console: https://www.yunqiai.chat
openai_base_url: https://www.yunqiai.chat/v1
anthropic_base_url: https://www.yunqiai.chat
gemini_generate_content_url: https://www.yunqiai.chat/v1beta/models/{model}:generateContent
models_endpoint: GET https://www.yunqiai.chat/v1/models
image_25_edits_url: https://www.yunqiai.chat/v1/images/edits?api-version=2025-04-01-preview
mj_generation_url: https://www.yunqiai.chat/v1/tob/diffusion
mj_job_url: https://www.yunqiai.chat/v1/tob/job/{jobId}
authentication:
  openai: "Authorization: Bearer YOUR_API_KEY"
  anthropic: "x-api-key: YOUR_API_KEY"
  gemini: "x-goog-api-key: YOUR_API_KEY"
```

### Base URL 与完整接口地址

- OpenAI SDK、Codex、Cherry Studio、Chatbox 等需要 Base URL 的客户端填写 `https://www.yunqiai.chat/v1`。
- Anthropic SDK 与 Claude Code 填写 `https://www.yunqiai.chat`，客户端会继续请求 `/v1/messages`。
- Gemini 原生调用使用完整地址 `https://www.yunqiai.chat/v1beta/models/{model}:generateContent`。除非客户端明确说明其路径拼接规则，不要把站点根地址直接当作 Gemini SDK Base URL。
- WorkBuddy 的自定义协议字段要求完整地址，填写 `https://www.yunqiai.chat/v1/chat/completions`。
- 不要在已经包含 `/v1` 的 Base URL 后再次拼接 `/v1`。

### 鉴权与 Content-Type

| 协议 | 鉴权请求头 | 常用 Content-Type |
|---|---|---|
| OpenAI Compatible、Responses、Images | `Authorization: Bearer YOUR_API_KEY` | JSON 请求使用 `application/json`；图片编辑使用 `multipart/form-data` |
| Anthropic Messages | `x-api-key: YOUR_API_KEY` | `application/json` |
| Gemini Generate Content | `x-goog-api-key: YOUR_API_KEY` | `application/json` |

Anthropic Messages 还需要：

```text
anthropic-version: 2023-06-01
```

## 2. 当前发布接口

| 场景 | 方法与路径 | 关键输入 | 鉴权 | 适用模型 |
|---|---|---|---|---|
| 获取模型列表 | `GET /v1/models` | 无请求体 | Bearer | 当前 API Key 可调用的模型 |
| OpenAI 对话 | `POST /v1/chat/completions` | `model`、`messages` | Bearer | GPT、Claude、Gemini 文本模型 |
| OpenAI Responses | `POST /v1/responses` | `model`、`input` | Bearer | GPT 系列 |
| Anthropic Messages | `POST /v1/messages` | `model`、`messages`、`max_tokens` | `x-api-key` | Claude 系列 |
| Gemini 文本与多模态 | `POST /v1beta/models/{model}:generateContent` | `contents` | `x-goog-api-key` | Gemini 文本模型 |
| Gemini 生图与参考图编辑 | `POST /v1beta/models/{model}:generateContent` | `contents`、`generationConfig.imageConfig` | `x-goog-api-key` | Gemini Image 模型 |
| OpenAI 图片生成 | `POST /v1/images/generations` | `model`、`prompt` | Bearer | `gpt-image-2` |
| OpenAI 图片编辑 | `POST /v1/images/edits` | `model`、`prompt`、`image` | Bearer | `gpt-image-2` |
| GPT Image 2.5 生成 | `POST /v1/images/generations` | `model`、`prompt` | Bearer | `gpt-image-2.5-flare`、`gpt-image-2.5-sunburst` |
| GPT Image 2.5 编辑 | `POST /v1/images/edits?api-version=2025-04-01-preview` | `model`、`prompt`、multipart `image` / `image[]` | Bearer | 两款 GPT Image 2.5 |
| MJ 生成 | `POST /v1/tob/diffusion` | `model`、`text` | Bearer | model 可为 mj_imagine 或 youchuan-image |
| MJ 查询 | `GET /v1/tob/job/{jobId}` | 路径中传返回的任务 `id` | Bearer | MJ / 悠船任务 |

Gemini Image 的 `generateContent` 在当前 HTTP 响应中返回结果。GPT Images 的同步与流式格式见各型号章节；GPT Image 2.5 使用第 12A 节的同步 JSON 响应。MJ 为异步任务，必须查询到完成状态。MJ 的查询接口不能查询 GPT Images 或 Gemini Image 的生成请求。

## 3. 模型目录与能力

模型目录用于核对模型 ID 和协议。实现时先调用 `GET /v1/models`，因为不同 API Key 可见的模型可能不同。用户自行决定调用型号。

| 模型 ID | 输入 | 输出 | 首选协议 | 主要用途或边界 |
|---|---|---|---|---|
| `gpt-6-astra` | 文本、图片、PDF | 文本 | Responses；文本对话也可用 Chat Completions | 推理、工具调用、结构化输出；见第 11 节 |
| `gpt-5.6-sol` | 文本、图片 | 文本 | Chat Completions | 复杂推理、高难度代码、Agent 工作流与图片理解 |
| `gpt-5.6-terra` | 文本 | 文本 | Chat Completions | 通用对话与 Agent 工作流，兼顾效果、速度与成本 |
| `gpt-5.6-luna` | 文本、图片 | 文本 | Chat Completions | 低延迟、图片理解、分类、提取、改写与高频轻量任务 |
| `gpt-5.5` | 文本、图片、文件 | 文本 | Responses | 图片理解、文件任务、工具调用、结构化输出 |
| `claude-sonnet-4-6` | 文本、图片 | 文本 | Anthropic Messages | 代码、长文本、图片理解与工具调用 |
| `claude-opus-4-6` | 文本 | 文本 | Anthropic Messages | 复杂推理与长上下文任务 |
| `claude-opus-4-7` | 文本、图片、文件 | 文本 | Anthropic Messages | 推理、工具、图片与文件任务 |
| `claude-opus-4-8` | 文本 | 文本 | Anthropic Messages | 高阶推理与大型代码任务 |
| `claude-sonnet-5` | 文本 | 文本 | Anthropic Messages | 日常开发、审阅与通用推理 |
| `claude-fable-5` | 文本 | 文本 | Anthropic Messages | 高难度代码、架构理解与长任务 |
| `gemini-3.5-flash` | 文本、图片、视频、音频、PDF | 文本 | Gemini Generate Content | 低延迟多模态；上下文 1,048,576 tokens；输出上限 65,536 tokens |
| `gemini-3.1-pro-preview` | 文本、图片、视频、音频、PDF | 文本 | Gemini Generate Content | 复杂多模态推理；上下文 1,048,576 tokens；输出上限 65,536 tokens |
| `gemini-3.1-flash-image` | 文本、参考图 | 图片、文本 | Gemini Generate Content | 512、1K、2K、4K；最多 14 张参考图 |
| `gemini-3-pro-image-preview` | 文本、参考图 | 图片、文本 | Gemini Generate Content | 1K、2K、4K；最多 14 张参考图 |
| `gpt-image-2` | 文本、参考图、蒙版 | 图片 | Images Generations / Edits | 文本生图、参考图编辑、多图合成与局部重绘 |
| `gpt-image-2.5-flare` | 文本、参考图、蒙版 | 图片 | Images Generations / 带 api-version 的 Edits | 五档质量及 auto、单图与多参考图；见 12A |
| `gpt-image-2.5-sunburst` | 文本、参考图、蒙版 | 图片 | Images Generations / 带 api-version 的 Edits | 五档质量及 auto、单图与多参考图；见 12A |
| MJ / 悠船 | 文本、图片 URL | 异步图片 URL | `/v1/tob/diffusion` 与 `/v1/tob/job/{jobId}` | 模型列表可见 mj_imagine、youchuan-image；生成请求使用 text；见 12B |
| `glm-5.2` | — | — | 联系商务 | 已灰度下线，如有需要请联系商务 |

`glm-5.2` 的接入范围与调用配置以商务确认信息为准。Agent 不应自动尝试调用该模型，也不要自行替换为其他型号。

## 4. GET `/v1/models`

返回当前 API Key 可见的模型目录。可见不等于该分组始终有可用渠道，使用最小请求确认调用权限。客户端模型选择器使用 `data[].id`，不要把展示名称当作模型 ID。

### 请求

```bash
curl https://www.yunqiai.chat/v1/models \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### 响应结构

```json
{
  "object": "list",
  "data": [
    {"id": "gpt-6-astra", "object": "model"},
    {"id": "gpt-5.6-sol", "object": "model"},
    {"id": "claude-sonnet-4-6", "object": "model"},
    {"id": "gemini-3.5-flash", "object": "model"}
  ]
}
```

### Agent 处理要求

- 使用 `data[].id` 填充模型列表。
- 切换 API Key 后重新读取模型列表。
- 收到 `model_not_found` 时重新读取列表，并检查是否使用了正确分组的 API Key。

## 5. POST `/v1/chat/completions`

OpenAI 兼容对话接口。GPT、Claude 与 Gemini 文本模型可以使用此接口；模型专属能力优先使用上方模型表中的首选协议。

### 请求参数

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `model` | `string` | 是 | 使用 `GET /v1/models` 返回的完整模型 ID |
| `messages` | `array` | 是 | `system`、`user`、`assistant`、`tool` 消息数组 |
| `max_completion_tokens` | `integer` | 否 | 最大生成 Token；上限随模型变化 |
| `stream` | `boolean` | 否 | 是否使用 SSE 流式返回；默认 `false` |
| `temperature` | `number` | 否 | `0–2`；推理模型建议省略，使用模型默认值 |
| `top_p` | `number` | 否 | `0–1`；通常与 `temperature` 只设置一个 |
| `n` | `integer` | 否 | 返回候选数量；通常使用 `1` 控制费用 |
| `stop` | `string` 或 `string[]` | 否 | 停止序列；使用模型默认行为时省略 |
| `tools` | `array` | 否 | OpenAI Function Calling 工具定义 |
| `tool_choice` | `string` 或 `object` | 否 | `auto`、`none`、`required` 或指定工具 |
| `response_format` | `object` | 否 | 普通文本或 JSON Schema 结构化输出 |

### 最小请求

```bash
curl --request POST \
  --url https://www.yunqiai.chat/v1/chat/completions \
  --header "Authorization: Bearer YOUR_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "model": "gpt-5.6-sol",
    "messages": [
      {"role": "user", "content": "你好，请介绍一下你自己"}
    ],
    "stream": false
  }'
```

### 图片输入

OpenAI 兼容消息使用 `image_url` 内容块。Base64 必须写成完整 Data URL。

```json
{
  "model": "gpt-5.6-sol",
  "messages": [
    {
      "role": "user",
      "content": [
        {"type": "text", "text": "描述这张图片"},
        {
          "type": "image_url",
          "image_url": {
            "url": "data:image/png;base64,BASE64_IMAGE_DATA",
            "detail": "low"
          }
        }
      ]
    }
  ]
}
```

### 响应结构

```json
{
  "id": "chatcmpl_01JY7X",
  "object": "chat.completion",
  "model": "gpt-5.6-sol",
  "choices": [
    {
      "index": 0,
      "message": {"role": "assistant", "content": "你好！我是一个 AI 助手。"},
      "finish_reason": "stop"
    }
  ]
}
```

非流式文本通常读取 `choices[0].message.content`。如果 `n` 大于 `1`，遍历全部 `choices[]`。

## 6. POST `/v1/responses`

OpenAI Responses 原生请求格式，适用于 GPT 系列。

### 请求参数

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `model` | `string` | 是 | 要调用的 GPT 模型 ID |
| `input` | `string` 或 `array` | 是 | 文本、消息、图片或 `input_file` 文件输入项 |
| `max_output_tokens` | `integer` | 否 | 从 `16` 起；最大值随模型变化 |
| `stream` | `boolean` | 否 | 是否使用 SSE 流式返回；默认 `false` |
| `temperature` | `number` | 否 | `0–2`；推理模型建议省略，使用模型默认值 |
| `top_p` | `number` | 否 | `0–1`；通常与 `temperature` 只设置一个 |
| `tools` | `array` | 否 | 客户端执行的 `function` 工具定义；图片生成使用 Images 端点 |
| `metadata` | `object` | 否 | 业务元数据 |

### 最小请求

```bash
curl --request POST \
  --url https://www.yunqiai.chat/v1/responses \
  --header "Authorization: Bearer YOUR_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "model": "gpt-5.6-sol",
    "input": "用三句话解释量子计算",
    "max_output_tokens": 1024,
    "stream": false
  }'
```

### Base64 图片输入

Responses 的 `input_image.image_url` 使用完整 Data URL，不要只传裸 Base64。

```json
{
  "model": "gpt-5.6-luna",
  "input": [
    {
      "role": "user",
      "content": [
        {"type": "input_text", "text": "描述这张图片"},
        {
          "type": "input_image",
          "image_url": "data:image/png;base64,BASE64_IMAGE_DATA",
          "detail": "low"
        }
      ]
    }
  ],
  "max_output_tokens": 256
}
```

### 文件输入（`gpt-5.5`）

`gpt-5.5` 的 Responses 请求使用 `input_file` 内容项。每个文件只选择一种来源：

| 来源 | 必需字段 | 写法 |
|---|---|---|
| 内联文件 | `type`、`filename`、`file_data` | `file_data` 使用完整 Data URL，例如 `data:application/pdf;base64,...` |
| 外部地址 | `type`、`file_url` | 使用服务端可以访问的完整 HTTPS URL |

内联 PDF 请求示例：

```json
{
  "model": "gpt-5.5",
  "input": [
    {
      "role": "user",
      "content": [
        {
          "type": "input_file",
          "filename": "brief.pdf",
          "file_data": "data:application/pdf;base64,BASE64_FILE_DATA"
        },
        {
          "type": "input_text",
          "text": "提取这份文件的关键结论。"
        }
      ]
    }
  ],
  "max_output_tokens": 1024
}
```

外部 PDF 使用以下文件项；URL 必须可由服务端访问：

```json
{"type": "input_file", "file_url": "https://example.com/brief.pdf"}
```

不使用 `file_id` 或 `/v1/files` 上传流程，其他服务商的文件 ID 不能跨服务复用。Base64 体积约为原文件的 4/3，还需计算 JSON 和文本开销；不要把单文件限制当成整次请求限制。格式或大小被拒绝后，转换为 PDF 或拆分再提交。

DOCX、PPTX 先在客户端转换为 PDF，再按上述文件字段发送。TXT、CSV、XLSX 可先提取文本或所需行列，说明列名和单位，再放入 `input_text`。不要把 Office 文件直接换成 PDF 的 MIME 类型。

文件接入验收需要同时检查响应状态和内容：使用含有已知标题、数字或标识的文件，确认回答确实引用了附件中的信息。HTTP 200、非空文本或 `status=completed` 都不能单独证明文件已被读取。若回答要求重新提供已经附带的文件，或无法回答附件中的已知事实，应按文件理解失败处理，不交付为摘要。保留请求 ID，联系支持确认当前密钥分组下该模型的文件处理能力；不要自动换模型或重复发送计费请求。

### 响应读取

原始 HTTP JSON 使用类型化的 `output[]`，例如：

```json
{
  "id": "resp_01JY7X",
  "object": "response",
  "model": "gpt-5.5",
  "output": [
    {
      "id": "rs_01JY7X",
      "type": "reasoning",
      "summary": []
    },
    {
      "id": "msg_01JY7X",
      "type": "message",
      "status": "completed",
      "role": "assistant",
      "content": [
        {
          "type": "output_text",
          "text": "这是模型返回的文本。",
          "annotations": []
        }
      ]
    }
  ]
}
```

OpenAI SDK 提供的 `response.output_text` 是把文本内容聚合后的便利属性，不是需要从原始 HTTP JSON 顶层读取的独立字段。只需要最终文本时可使用 SDK 的 `output_text`；处理推理、工具或多模态输出时，应遍历 `response.output[]`，按每个 Item 的 `type` 分支，再遍历消息 Item 的 `content[]`。

多轮会话在客户端保存历史。下一轮发送此前 `input`、上一轮完整 `output[]` 与新增用户消息；不要只保留可见文本而丢弃 reasoning 或工具项。不要发送 `previous_response_id`，也不要依赖 `GET /v1/responses/{id}` 恢复会话。请求返回 200 不代表上一轮上下文已被保存。

## 7. POST `/v1/messages`

Anthropic 原生 Messages API，适用于 Claude 系列。

### 必需请求头

```text
x-api-key: YOUR_API_KEY
anthropic-version: 2023-06-01
Content-Type: application/json
```

### 请求参数

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `model` | `string` | 是 | 使用 `GET /v1/models` 返回的完整 Claude 模型 ID |
| `messages` | `array` | 是 | `user` 与 `assistant` 消息数组；图片使用 `image`，文档使用 `document` 内容块 |
| `max_tokens` | `integer` | 是 | 最大输出 Token，填写正整数；上限随模型变化 |
| `system` | `string` 或 `array` | 否 | 顶层系统提示；Messages API 没有 `system` 角色 |
| `stream` | `boolean` | 否 | 是否使用 SSE 流式返回；默认 `false` |
| `temperature` | `number` | 否 | `0–1`；Claude 4.7 及以后模型建议省略采样参数 |
| `top_p` | `number` | 否 | `0–1`；通常与 `temperature` 只设置一个 |
| `stop_sequences` | `string[]` | 否 | 自定义停止序列 |
| `tools` | `array` | 否 | Anthropic 工具定义 |

### 最小请求

```bash
curl --request POST \
  --url https://www.yunqiai.chat/v1/messages \
  --header "x-api-key: YOUR_API_KEY" \
  --header "anthropic-version: 2023-06-01" \
  --header "Content-Type: application/json" \
  --data '{
    "model": "claude-sonnet-4-6",
    "max_tokens": 1024,
    "messages": [
      {"role": "user", "content": "你好"}
    ]
  }'
```

### Base64 图片输入

Claude 图片使用 `image` 内容块。`source.data` 填裸 Base64，MIME 类型单独填写在 `source.media_type`。

```json
{
  "model": "claude-sonnet-4-6",
  "max_tokens": 256,
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "image",
          "source": {
            "type": "base64",
            "media_type": "image/png",
            "data": "BASE64_IMAGE_DATA"
          }
        },
        {"type": "text", "text": "描述这张图片"}
      ]
    }
  ]
}
```

图片输入范围：

- 支持 JPEG、PNG、GIF、WebP。
- 单张图片控制在 `8000×8000` 以内。
- Base64 编码后单张图片小于 `10 MB`。
- 整次请求体控制在 `32 MB` 以内。

### 文档输入（`claude-opus-4-7`）

Anthropic Messages 使用 `document` 内容块内联 PDF：

| `source.type` | 必需字段 | 说明 |
|---|---|---|
| `base64` | `media_type`、`data` | `media_type` 填 `application/pdf`，`data` 填裸 Base64 |

可直接发送的 Base64 PDF 请求：

```json
{
  "model": "claude-opus-4-7",
  "max_tokens": 1024,
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "document",
          "source": {
            "type": "base64",
            "media_type": "application/pdf",
            "data": "BASE64_PDF_DATA"
          }
        },
        {
          "type": "text",
          "text": "总结这份文档，并列出三个关键结论。"
        }
      ]
    }
  ]
}
```

不使用 `/v1/files`、`file_id` 或外部 PDF URL。将本地文件编码后放入 `source.data`；不要添加 Data URL 前缀。较大的 PDF 先拆分，再分别提交。Base64 体积约增加 1/3，整次请求还包括提示词与其他内容块。

### 响应结构

```json
{
  "id": "msg_01JY7X",
  "type": "message",
  "role": "assistant",
  "model": "claude-sonnet-4-6",
  "content": [
    {"type": "text", "text": "你好！我是一个 AI 助手。"}
  ],
  "stop_reason": "end_turn"
}
```

遍历 `content[]`，分别处理文本块、工具调用块及其他协议内容块。

## 8. POST `/v1beta/models/{model}:generateContent`

Gemini 原生 Generate Content。文本、多模态理解和 Gemini 生图模型共用这一路径；模型 ID 位于 URL 中。

### 文本与多模态参数

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `contents` | `array` | 是 | Content 数组，每项含 `role` 与 `parts` |
| `systemInstruction` | `object` | 否 | 系统指令，结构同 Content |
| `generationConfig.maxOutputTokens` | `integer` | 否 | 最大输出 Token；上限随模型变化 |
| `generationConfig.temperature` | `number` | 否 | 通常为 `0–2`；默认值和范围以模型为准 |
| `generationConfig.topP` | `number` | 否 | `0–1` 的核采样阈值 |
| `generationConfig.stopSequences` | `string[]` | 否 | 停止序列 |
| `safetySettings` | `array` | 否 | 按危害类别设置安全阈值 |

### 文本请求

```bash
curl --request POST \
  --url https://www.yunqiai.chat/v1beta/models/gemini-3.5-flash:generateContent \
  --header "x-goog-api-key: YOUR_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "contents": [
      {"role": "user", "parts": [{"text": "你好，请介绍一下你自己"}]}
    ],
    "generationConfig": {"maxOutputTokens": 1024}
  }'
```

### 内联媒体格式

媒体放在 `contents[].parts[].inlineData` 中，`data` 只放裸 Base64，`mimeType` 填实际 MIME 类型。媒体、提示词和系统指令合计控制在 `20 MB` 以内。

| 输入 | 支持格式与写法 | 用途 |
|---|---|---|
| 图片 | PNG、JPEG、WebP、HEIC、HEIF；`inlineData` | 识图、OCR、图表与界面分析 |
| 视频 | MP4、MPEG、MOV、AVI、FLV、MPG、WebM、WMV、3GPP；`inlineData` | 摘要、事件提取、时间点问答 |
| 音频 | 使用实际音频 MIME 类型；`inlineData` | 转写、摘要、说话内容分析 |
| PDF | `application/pdf`；`inlineData` | 文档、扫描件与表格理解 |

### 图片输入

```json
{
  "contents": [
    {
      "parts": [
        {"text": "描述这张图片"},
        {
          "inlineData": {
            "mimeType": "image/png",
            "data": "BASE64_IMAGE_DATA"
          }
        }
      ]
    }
  ],
  "generationConfig": {"maxOutputTokens": 2048}
}
```

输出预算需要同时容纳思考过程与最终回答。读取每个候选的 `finishReason`；`MAX_TOKENS` 表示预算耗尽，即使 HTTP 200 且已有文字，也不能标记为完整回答。保留部分结果和用量，提示调用方提高 `maxOutputTokens` 或缩小任务范围后重新发起请求，不自动重放。本例的 2048 是起始预算，不保证复杂任务一定完成。没有候选或没有所需输出时，检查 `promptFeedback` 和候选结束原因，不按空结果成功处理。

### 视频参数

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `inlineData.mimeType` | `string` | 是 | 例如 `video/mp4` |
| `inlineData.data` | `string` | 是 | 裸 Base64，不带 `data:video/...` 前缀 |
| `videoMetadata.startOffset` | `duration` | 否 | 分析起点，例如 `40s` |
| `videoMetadata.endOffset` | `duration` | 否 | 分析终点，例如 `80s` |
| `videoMetadata.fps` | `number` | 否 | 默认约 `1 FPS`；长视频可低于 `1`，快速动作可适当提高 |

```json
{
  "contents": [
    {
      "parts": [
        {
          "inlineData": {
            "mimeType": "video/mp4",
            "data": "BASE64_VIDEO_DATA"
          },
          "videoMetadata": {
            "startOffset": "0s",
            "endOffset": "30s",
            "fps": 1
          }
        },
        {"text": "概括视频中的主要事件，并给出对应时间点。"}
      ]
    }
  ],
  "generationConfig": {"maxOutputTokens": 2048}
}
```

较长视频先压缩或按时间段裁切，再使用 `startOffset` 与 `endOffset` 分段分析。复杂视频任务可从 `maxOutputTokens: 2048` 起设置。

### 文本响应结构

```json
{
  "candidates": [
    {
      "content": {
        "role": "model",
        "parts": [{"text": "你好！我是一个 AI 助手。"}]
      },
      "finishReason": "STOP"
    }
  ],
  "modelVersion": "gemini-3.5-flash"
}
```

遍历 `candidates[].content.parts[]`，分别读取 `text` 或 `inlineData`。

## 9. Base64 与文件上传对照

| 协议 | 输入字段 | 传值形式 | 输出图片字段 |
|---|---|---|---|
| Chat Completions | `messages[].content[].image_url.url` | 完整 Data URL：`data:image/png;base64,...` | 视觉理解返回文本 |
| Responses | `input_image.image_url` | 完整 Data URL：`data:image/png;base64,...` | 视觉理解返回文本 |
| Responses 文件 | `input_file.file_data` | 完整 Data URL，并同时填写 `filename` | 文件理解返回文本 |
| Anthropic Messages | `source.data` | 裸 Base64，并填写 `source.media_type` | 视觉理解返回文本 |
| Anthropic PDF | `document.source.data` | 裸 Base64，`source.type` 为 `base64`，`media_type` 为 `application/pdf` | 文档理解返回文本 |
| Gemini Generate Content | `inlineData.data` | 裸 Base64，并填写 `inlineData.mimeType` | Gemini 生图：`parts[].inlineData.data` |
| OpenAI Images Edits | `image` 或重复的 `image[]` | `multipart/form-data` 文件 | `data[].b64_json` 或 `data[].url` |

如果现有图片只有 Base64，而接口要求 multipart 文件，先将 Base64 解码为图片文件，再上传到 `image` 字段。

## 10. Gemini Image：生成与参考图编辑

适用模型：

- `gemini-3.1-flash-image`
- `gemini-3-pro-image-preview`

端点：

```text
POST /v1beta/models/{model}:generateContent
```

只传文字时生成图片；在同一个 `parts` 数组中加入 `inlineData` 时进行参考图生成或编辑。结果在当前请求中直接返回。

### 请求参数

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `contents` | `array` | 是 | 文本与参考图片输入 |
| `contents[].role` | `string` | 否 | `user` 或 `model`；单轮可省略，多轮按对话顺序填写 |
| `contents[].parts[].text` | `string` | 否 | 提示词；Flash 输入上下文最多 131,072 tokens，Pro 最多 65,536 tokens |
| `contents[].parts[].inlineData` | `object` | 否 | Base64 参考图，可与文字放在同一个 `parts` 数组中 |
| `systemInstruction` | `object` | 否 | 系统指令，计入输入上下文与请求体大小 |
| `generationConfig.responseModalities` | `string[]` | 否 | `TEXT`、`IMAGE` 或二者；只要图片可设为 `["IMAGE"]` |
| `generationConfig.imageConfig.aspectRatio` | `string` | 否 | 从对应模型的比例表中选择 |
| `generationConfig.imageConfig.imageSize` | `string` | 否 | Flash：`512`、`1K`、`2K`、`4K`；Pro：`1K`、`2K`、`4K`；`K` 必须大写 |
| `safetySettings` | `array` | 否 | 安全策略；同一类别不要重复传入 |
| `inlineData.mimeType` | `string` | 图片输入时是 | `image/png`、`image/jpeg`、`image/webp`、`image/heic`、`image/heif` |
| `inlineData.data` | `string` | 图片输入时是 | 裸 Base64，不带 `data:image/...` 前缀 |

### `gemini-3.1-flash-image` 比例与像素

| `aspectRatio` | `512` | `1K` | `2K` | `4K` |
|---|---:|---:|---:|---:|
| `1:1` | 512×512 | 1024×1024 | 2048×2048 | 4096×4096 |
| `1:4` | 256×1024 | 512×2048 | 1024×4096 | 2048×8192 |
| `1:8` | 192×1536 | 384×3072 | 768×6144 | 1536×12288 |
| `2:3` | 424×632 | 848×1264 | 1696×2528 | 3392×5056 |
| `3:2` | 632×424 | 1264×848 | 2528×1696 | 5056×3392 |
| `3:4` | 448×600 | 896×1200 | 1792×2400 | 3584×4800 |
| `4:1` | 1024×256 | 2048×512 | 4096×1024 | 8192×2048 |
| `4:3` | 600×448 | 1200×896 | 2400×1792 | 4800×3584 |
| `4:5` | 464×576 | 928×1152 | 1856×2304 | 3712×4608 |
| `5:4` | 576×464 | 1152×928 | 2304×1856 | 4608×3712 |
| `8:1` | 1536×192 | 3072×384 | 6144×768 | 12288×1536 |
| `9:16` | 384×688 | 768×1376 | 1536×2752 | 3072×5504 |
| `16:9` | 688×384 | 1376×768 | 2752×1536 | 5504×3072 |
| `21:9` | 792×168 | 1584×672 | 3168×1344 | 6336×2688 |

### `gemini-3-pro-image-preview` 比例与像素

| `aspectRatio` | `1K` | `2K` | `4K` |
|---|---:|---:|---:|
| `1:1` | 1024×1024 | 2048×2048 | 4096×4096 |
| `2:3` | 848×1264 | 1696×2528 | 3392×5056 |
| `3:2` | 1264×848 | 2528×1696 | 5056×3392 |
| `3:4` | 896×1200 | 1792×2400 | 3584×4800 |
| `4:3` | 1200×896 | 2400×1792 | 4800×3584 |
| `4:5` | 928×1152 | 1856×2304 | 3712×4608 |
| `5:4` | 1152×928 | 2304×1856 | 4608×3712 |
| `9:16` | 768×1376 | 1536×2752 | 3072×5504 |
| `16:9` | 1376×768 | 2752×1536 | 5504×3072 |
| `21:9` | 1584×672 | 3168×1344 | 6336×2688 |

### 参考图能力与请求大小

| 模型 | 参考图总数 | 物体保持 | 人物一致性 | 风格参考 |
|---|---:|---:|---:|---:|
| `gemini-3.1-flash-image` | 最多 14 张 | 最多 10 个 | 最多 4 个角色 | 随参考图一并描述 |
| `gemini-3-pro-image-preview` | 最多 14 张 | 最多 6 个 | 最多 5 个角色 | 最多 3 张 |

内联图片、提示词和系统指令合计控制在 `20 MB` 以内。

### 文生图请求

```json
{
  "contents": [
    {
      "parts": [
        {"text": "生成一张雨后的未来城市，霓虹倒影，电影感构图"}
      ]
    }
  ],
  "generationConfig": {
    "responseModalities": ["IMAGE"],
    "imageConfig": {
      "aspectRatio": "16:9",
      "imageSize": "2K"
    }
  }
}
```

### 参考图请求

```json
{
  "contents": [
    {
      "parts": [
        {"text": "保留主体，把背景改成夜景"},
        {
          "inlineData": {
            "mimeType": "image/png",
            "data": "BASE64_IMAGE_DATA"
          }
        }
      ]
    }
  ],
  "generationConfig": {
    "responseModalities": ["IMAGE"],
    "imageConfig": {
      "aspectRatio": "1:1",
      "imageSize": "1K"
    }
  }
}
```

多轮连续编辑时，下一轮继续传入上一轮图片和新的文字要求。每轮明确说明“保留什么、只修改什么”。

### 响应结构

```json
{
  "candidates": [
    {
      "content": {
        "role": "model",
        "parts": [
          {
            "inlineData": {
              "mimeType": "image/png",
              "data": "BASE64_IMAGE_DATA"
            }
          }
        ]
      },
      "finishReason": "STOP"
    }
  ],
  "modelVersion": "gemini-3.1-flash-image"
}
```

遍历所有 `candidates[]` 和 `parts[]`；每遇到一个 `inlineData` 就按其 `mimeType` 选择扩展名，并将 `data` 解码保存。

## 11. `gpt-6-astra`

### 接口与字段

模型 ID 固定使用 `gpt-6-astra`。Base URL 为 `https://www.yunqiai.chat`，鉴权为 `Authorization: Bearer YOUR_API_KEY`。

| 场景 | 接口 | 参数 |
|---|---|---|
| 对话、工具调用、结构化输出、图片与 PDF | `POST /v1/responses` | `input`、`reasoning.effort`、`max_output_tokens` |
| 文本对话 | `POST /v1/chat/completions` | `messages`、`reasoning_effort`、`max_completion_tokens` |
| 工具定义与回传 | Responses | `tools`、`tool_choice`、`function_call_output` |
| JSON Schema | Responses | `text.format`，包含 `type=json_schema`、`name`、`schema`、`strict` |
| 流式返回 | Responses | `stream=true`；以 `response.completed` 为完成事件 |

`reasoning.effort` 支持 `low`、`medium`、`high`、`xhigh`、`max`，不要发送 `none` 或 `minimal`。不发送 `temperature`、`top_p`、`top_logprobs`；Chat Completions 也不发送 `logprobs`。输出预算包含推理与最终回答，预算耗尽时须检查 `status=incomplete`，不能当作完整回答。

工具调用使用 Responses，不把 Chat Completions 的 `tool_calls` 字段套入 Responses。模型返回 `function_call` 后由客户程序执行工具，并用同一个 `call_id` 回传 `function_call_output`。无状态续接时保留上一轮完整 `output[]`，不要只保留可见文本。完整协议见第 6、13、14 节。

### 请求示例

```python
import os
import requests

response = requests.post(
    "https://www.yunqiai.chat/v1/responses",
    headers={"Authorization": "Bearer " + os.environ["YUNQIAI_API_KEY"]},
    json={
        "model": "gpt-6-astra",
        "input": "用三句话解释量子计算。",
        "reasoning": {"effort": "low"},
        "max_output_tokens": 1024,
    },
    timeout=(15, 180),
)
response.encoding = "utf-8"
if not response.ok:
    request_id = response.headers.get("x-request-id") or response.headers.get("request-id")
    raise RuntimeError(f"HTTP {response.status_code}; request_id={request_id}; body={response.text[:2000]}")
result = response.json()
if result.get("error") or result.get("status") != "completed":
    raise RuntimeError(str(result.get("error") or result.get("incomplete_details") or result.get("status")))
for item in result.get("output", []):
    if item.get("type") == "message":
        for part in item.get("content", []):
            if part.get("type") == "output_text":
                print(part["text"])
```

图片输入使用 `input_image.image_url`，可传图片 Data URL。PDF 使用 `input_file`，同时提供 `filename` 和 `file_data=data:application/pdf;base64,...`。具体结构见第 6、9 节。音频与视频不作为此模型的直接输入，PDF 与图片输入也不使用 Images 生成端点。

### 客户端配置

Codex CLI / VS Code 的配置沿用第 16 节的 provider 与鉴权结构，将 `model` 设为 `gpt-6-astra`，`wire_api` 保持 `responses`，`model_reasoning_effort` 使用上面的合法档位。CC Switch 管理 Codex 时填写同样的模型 ID 与 `https://www.yunqiai.chat/v1`。不要将本模型填入仅支持 Anthropic Messages 的 Claude Code 原生配置。

参考：[官方模型参数](https://developers.openai.com/api/docs/models/gpt-6-astra)、[官方 API 参数约定](https://developers.openai.com/api/docs/guides/latest-model)。

## 12. `gpt-image-2`

支持文本生图、参考图编辑、图生图、多图合成和蒙版局部重绘。

### 端点

```text
POST /v1/images/generations
POST /v1/images/edits
```

文生图请求使用 `application/json`；参考图编辑请求使用 `multipart/form-data`。

### 文生图参数

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `model` | `string` | 是 | 固定为 `gpt-image-2` |
| `prompt` | `string` | 是 | `1–32,000` 字符 |
| `size` | `string` | 否 | `auto` 或符合约束的 `WIDTHxHEIGHT`；无需另传 `resolution` |
| `quality` | `string` | 否 | `low`、`medium`、`high`、`auto`；默认 `auto` |
| `n` | `integer` | 否 | `1–10`；生成数量会直接影响费用 |
| `output_format` | `string` | 否 | `png`、`jpeg`、`webp`；默认 `png` |
| `output_compression` | `integer` | 否 | `0–100`；仅 JPEG / WebP，默认 `100` |
| `background` | `string` | 否 | `auto` 或 `opaque`；默认 `auto` |
| `moderation` | `string` | 否 | `auto` 或 `low`；默认 `auto` |
| `stream` | `boolean` | 否 | 是否流式返回 |
| `partial_images` | `integer` | 否 | `0–3`；仅流式请求，默认 `0` |
| `user` | `string` | 否 | 终端用户标识，用于滥用监测 |

### 文生图请求

```bash
curl --request POST \
  --url https://www.yunqiai.chat/v1/images/generations \
  --header "Authorization: Bearer YOUR_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "model": "gpt-image-2",
    "prompt": "雨后的未来城市，霓虹倒影，电影感构图",
    "size": "1536x1024",
    "quality": "medium",
    "output_format": "png",
    "n": 1
  }'
```

### 参考图编辑参数

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `model` | `string` | 是 | 固定为 `gpt-image-2` |
| `image` / `image[]` | `file` 或 `file[]` | 是 | 上传 `1–16` 张 PNG、JPG 或 WebP 参考图；每张小于 `50 MB` |
| `mask` | `file` | 否 | PNG，小于 `4 MB`；与第一张参考图尺寸一致并含 Alpha 通道 |
| `prompt` | `string` | 是 | `1–32,000` 字符；说明需要保留和修改的画面内容 |

其余可选参数与上方文生图参数相同。

### 自定义尺寸规则

| 约束 | 范围 | 说明 |
|---|---:|---|
| 宽高步进 | 16 px | 宽和高都必须是 16 的倍数 |
| 输出比例 | 1:3–3:1 | 长边与短边之比不超过 3:1 |
| 最长边 | ≤ 3840 px | 横图和竖图使用同一限制 |
| 总像素 | 655,360–8,294,400 | 在范围内可自由组合宽高 |

常用尺寸：

| `size` | 比例 | 场景 |
|---|---:|---|
| `1024x1024` | 1:1 | 方形图片 |
| `1536x1024` | 3:2 | 横向图片 |
| `1024x1536` | 2:3 | 竖向图片 |
| `2048x2048` | 1:1 | 2K 方形图片 |
| `2048x1152` | 16:9 | 2K 横图 |
| `3840x2160` | 16:9 | 最大横图 |
| `2160x3840` | 9:16 | 最大竖图 |

### 质量

| `quality` | 特点 | 场景 |
|---|---|---|
| `auto` | 自动选择 | 不想手动指定质量时 |
| `low` | 速度最快、费用最低、细节较少 | 快速草稿、构图预览 |
| `medium` | 速度、费用与细节较均衡 | 日常生图与常规编辑 |
| `high` | 细节最丰富、生成更慢、费用较高 | 复杂场景、精细纹理与大量细节 |

### 多图与蒙版

- 多张参考图重复填写 `image[]`。
- 蒙版只作用于第一张参考图。
- 主体保持自动按高保真方式处理，无需额外参数。
- 蒙版用于指定主要修改区域；提示词同时写明位置、修改内容和需要保留的主体。

```bash
curl --request POST \
  --url https://www.yunqiai.chat/v1/images/edits \
  --header "Authorization: Bearer YOUR_API_KEY" \
  --form "model=gpt-image-2" \
  --form "image[]=@./scene.png" \
  --form "image[]=@./product.png" \
  --form "mask=@./mask.png" \
  --form "prompt=把第二张图中的商品放到第一张场景中，保持商品外观不变" \
  --form "size=1536x1024" \
  --form "quality=medium" \
  --form "n=1"
```

### 响应结构

```json
{
  "created": 1785987000,
  "data": [
    {"b64_json": "BASE64_IMAGE_DATA"}
  ],
  "output_format": "png",
  "quality": "medium",
  "size": "1536x1024"
}
```

当 `n` 大于 `1` 时，客户端必须遍历完整 `data[]`。不要只读取 `data[0]`。

### Python 解码全部图片

```python
import base64
import requests

for index, item in enumerate(response.data, start=1):
    b64_json = getattr(item, "b64_json", None)
    url = getattr(item, "url", None)
    if b64_json:
        image_bytes = base64.b64decode(b64_json)
    elif url:
        download = requests.get(url, timeout=(10, 300))
        download.raise_for_status()
        image_bytes = download.content
    else:
        raise ValueError("image item contains neither b64_json nor url")

    with open(f"result-{index}.png", "wb") as file:
        file.write(image_bytes)
```

## 12A. `gpt-image-2.5-flare` / `gpt-image-2.5-sunburst`

模型 ID 使用完整字符串，不省略后缀，不自动切换型号。本节参数适用于这两个型号。

### 接口与鉴权

| 操作 | 方法与路径 | 请求体 |
|---|---|---|
| 文生图 | `POST /v1/images/generations` | JSON |
| 参考图与蒙版编辑 | `POST /v1/images/edits?api-version=2025-04-01-preview` | multipart/form-data |

域名为 `https://www.yunqiai.chat`，鉴权头为 `Authorization: Bearer YOUR_API_KEY`。编辑时保留查询参数，multipart 的 boundary 由 HTTP 客户端生成。API 密钥需包含对应生图模型的权限。

### 参数

| 字段 | 类型 | 必填 / 默认 | 说明 |
|---|---|---|---|
| `model` | string | 必填 | `gpt-image-2.5-flare` 或 `gpt-image-2.5-sunburst` |
| `prompt` | string | 必填 | 生成或编辑指令，多图时按上传顺序说明各图作用 |
| `size` | string | `auto` | `auto` 或 `WIDTHxHEIGHT`，如 `1024x1024`、`1536x1024`、`1024x1536`、`1536x864` |
| `quality` | string | `auto` | `low`、`medium`、`high`、`xhigh`、`max`、`auto` |
| `n` | integer | `1` | 请求图片数量；可传 `2`，返回时遍历完整 `data[]` |
| `image` | file | 单图编辑必填 | PNG、JPEG、WebP 图片文件，不是文件路径字符串 |
| `image[]` | file[] | 多图编辑必填 | 重复同名 multipart 字段，例如两张参考图各占一个 `image[]` |
| `mask` | file | 编辑可选 | 带 Alpha 通道的 PNG，与第一张参考图宽高一致，透明区域为编辑区域 |
| `output_format` | string | `png` | `png`、`jpeg`；不使用 `webp` 输出 |
| `output_compression` | integer | 可选 | `0–100`，仅 JPEG 输出使用 |
| `background` | string | `auto` | `auto`、`opaque`、`transparent`；透明背景使用 PNG |
| `moderation` | string | `auto` | `auto`、`low`；不豁免内容政策 |
| `response_format` | 不发送 | — | 图片直接由响应 `data[].b64_json` 返回 |
| `input_fidelity` | 不发送 | — | 这两个型号不使用该字段 |

指定尺寸使用小写 `x` 分隔，宽高按 16 对齐，长短边比例不超过 3:1。`auto` 由服务决定尺寸；显示和排版读取实际解码后的宽高，不假定为正方形。不要额外发送 `resolution`。

`quality` 控制生成档位，`size` 控制像素尺寸。固定档位需显式传入；`auto` 不保证每次选择同一档位。返回用量读取 `usage`，费用以控制台账单为准，不从图片文件体积推算。

### 参考图与蒙版

参考图通过独立文件字段传入，不把 Base64 拼进 `prompt`。多图请求使用重复 `image[]`，提示词中的图 1、图 2 按上传顺序对应。蒙版作用于第一张参考图，其他参考图提供辅助内容。生成式编辑可能重新绘制画面，不保证蒙版外逐像素不变。

```bash
curl --fail-with-body --max-time 300 \
  'https://www.yunqiai.chat/v1/images/edits?api-version=2025-04-01-preview' \
  -H "Authorization: Bearer $YUNQIAI_API_KEY" \
  -F "model=$YUNQIAI_IMAGE_MODEL" \
  -F 'prompt=保留图1的构图，将图2的杯子放在画面中间' \
  -F 'image[]=@reference.png;type=image/png' \
  -F 'image[]=@product.png;type=image/png' \
  -F 'size=1024x1024' -F 'quality=low' -F 'n=1' \
  -o result.json
```

局部编辑额外添加 `-F 'mask=@mask.png;type=image/png'`。上述命令适用于 Bash / WSL；PowerShell 使用 `curl.exe`，环境变量写为 `$env:YUNQIAI_API_KEY`，换行使用反引号。

### 完整 Python 示例

依赖：`python -m pip install requests pillow`。设置 `YUNQIAI_API_KEY`、`YUNQIAI_IMAGE_MODEL`；`YUNQIAI_IMAGE_MODE` 可为 `generate`、`edit`、`multi`。编辑示例读取当前目录的 `reference.png`，多图另外读取 `product.png`。

```python
import base64
import io
import os
from contextlib import ExitStack
from pathlib import Path

import requests
from PIL import Image

base = "https://www.yunqiai.chat"
model = os.environ["YUNQIAI_IMAGE_MODEL"]
if model not in {"gpt-image-2.5-flare", "gpt-image-2.5-sunburst"}:
    raise ValueError("Use a complete GPT Image 2.5 model ID")
headers = {"Authorization": "Bearer " + os.environ["YUNQIAI_API_KEY"]}
mode = os.environ.get("YUNQIAI_IMAGE_MODE", "generate")
payload = {
    "model": model,
    "prompt": "A blue ceramic mug with a yellow square on a white table",
    "size": "1024x1024",
    "quality": "low",
    "n": 1,
}

if mode == "generate":
    response = requests.post(base + "/v1/images/generations",
                             headers=headers, json=payload, timeout=(15, 300))
elif mode in {"edit", "multi"}:
    paths = ["reference.png"] if mode == "edit" else ["reference.png", "product.png"]
    payload["prompt"] = "Keep the blue rectangle and YQ-A1 from the first image; change its red circle to green."
    if mode == "multi":
        payload["prompt"] += " Add the blue mug with its yellow square from the second image at the center."
    with ExitStack() as stack:
        files = [("image" if mode == "edit" else "image[]",
                  (Path(path).name, stack.enter_context(open(path, "rb")), "image/png"))
                 for path in paths]
        response = requests.post(
            base + "/v1/images/edits?api-version=2025-04-01-preview",
            headers=headers, data=payload, files=files, timeout=(15, 300))
else:
    raise ValueError("mode must be generate, edit or multi")

# Retain the error body before raising; do not log request headers.
response.encoding = "utf-8"
if not response.ok:
    raise RuntimeError(f"HTTP {response.status_code}: {response.text[:1000]}")
result = response.json()
if result.get("error") or not result.get("data"):
    raise RuntimeError("No generated images in response")
for index, item in enumerate(result["data"]):
    raw = base64.b64decode(item["b64_json"], validate=True)
    image = Image.open(io.BytesIO(raw))
    image.load()
    path = Path(f"result-{index}.{image.format.lower()}")
    path.write_bytes(raw)
    print(path, image.size, result.get("quality"), result.get("usage"))
```

### 响应与错误

成功响应的图片位于 `data[].b64_json`，先解码再识别文件格式，不根据文件名猜测。同步响应同时提供 `size`、`quality`、`output_format` 和 `usage`。自动尺寸和自动质量以返回结果为准。

检查 HTTP 状态、`error` 对象、图片数组、实际数量与解码结果。`usage.output_tokens_details.image_tokens` 为图像输出用量。输入格式支持 WebP 不代表输出也支持 WebP，不能把两个字段的枚举混为一谈。

连接超时或读取超时且尚未收到完整结果时，生成结果未知，不自动重发。此接口不使用 MJ 任务查询路径。错误信息记录 `request_id` 等排查信息，不记录请求鉴权头。

## 12B. MJ / 悠船：图片生成与编辑

### 接入约定

Base URL：`https://www.yunqiai.chat`。本章密钥从 `YOUCHUAN_API_KEY` 环境变量读取，值为开通 MJ 权限的 YunQi API 密钥，不是上游网站的机构密钥。生成与编辑发送 JSON。

```http
Authorization: Bearer <YOUCHUAN_API_KEY>
Content-Type: application/json
Accept: application/json
```

不使用 `x-api-key`、`x-goog-api-key`、`x-youchuan-secret` 或 `x-youchuan-app`。环境变量名称只供客户端读取，不作为 HTTP 请求头名称。不要将密钥写进 URL、提示词、回调地址或参考图链接。

密钥分组需要包含相应 MJ 图片权限。MJ 文生图请求的 `model` 可使用 `mj_imagine` 或 `youchuan-image`，两个名称都可调用同一 MJ 接口；提示词字段为 `text`。版本由 `text` 末尾的 `--v` 或 `--niji` 指定，不写进 `model`。`/v1/tob/subscribe` 不用于生成；提交使用 `/v1/tob/diffusion`。


### 文生图请求

```json
{
  "model": "mj_imagine",
  "text": "现代东方建筑，清晨薄雾，电影级建筑摄影，无文字 --v 8.1 --fast --hd --q 4 --raw --ar 16:9"
}
```

POST `/v1/tob/diffusion`。提示词字段为 `text`，不是 `prompt`。只使用 `model`、`text` 与可选的 `callback` 请求体字段；`size`、`n`、`quality`、`aspectRatio`、`response_format` 不是此接口的生成参数，不要发送。版本、比例、质量等 MJ 参数均放在 `text` 末尾，参数前留空格，末尾不加逗号或句号。

### 版本与质量

以下为 YunQi MJ 接口的版本选择与参数写法。版本使用 `--v` 或 `--niji` 二选一，并显式指定；未指定时该接口默认使用 v7。不要用 Midjourney 网站的默认版本替代本接口约定。

| 版本参数 | `--q` 可用值 | 说明 |
|---|---|---|
| `--v 5.1`、`--v 5.2`、`--v 6` | `0.25`、`0.5`、`1` | 默认 1 |
| `--v 6.1` | `0.5`、`1`、`2` | 默认 1 |
| `--v 7` | `1`、`2`、`4` | 默认 1 |
| `--v 8`、`--v 8.1` | `1`、`4` | 默认 1 |
| `--v 8.2` | `1`、`2`、`3`、`4` | 默认 1 |
| `--niji 5`、`--niji 6` | `0.25`、`0.5`、`1` | 默认 1 |
| `--niji 7` | 不发送 | 不支持 `--q` |

`--q` 是 MJ 的生成质量参数，不是 OpenAI Images 的 `quality=low/medium/high`，也不是像素尺寸。它不承诺画面得分、手指正确率或逐档可见提升；输出尺寸读取实际图片。请求受理、响应回显参数或相同费用均不能独立证明底层的计算档位。

### 参数字段

这些是 `text` 内的参数，不是独立 JSON 字段。“通用”不表示每个历史版本都支持全部参数；带版本限制的参数需同时满足下表与组合约束。

| 参数 | 值与默认值 | 用途与约束 |
|---|---|---|
| `--ar W:H` | 正整数比例，例如 `16:9`、`1:1`、`9:16`、`4:3`、`2:3`；默认 1:1 | 宽高比，不是固定像素宽高；V8.1/V8.2 的 SD 最大 14:1、HD 最大 4:1（含纵向对称比例） |
| `--raw` | 开关 | 减少默认风格化；不同历史版本的 Raw 写法与兼容性不同 |
| `--chaos` | 0–100；默认 0 | 同次生成的变化程度 |
| `--seed` | 0–4294967295 的整数 | 随机种子；不保证跨版本、模式或服务更新后逐像素复现 |
| `--stylize` / `--s` | 0–1000；默认 100 | 风格化强度 |
| `--weird` / `--w` | 0–3000；默认 0 | 非常规造型程度 |
| `--no` | 要排除的内容 | 负向描述，不保证绝对排除 |
| `--tile` | 开关 | 无缝平铺；不与 v7 draft 组合 |
| `--fast` | 开关 | 快速模式；不同时发送 `--turbo` |
| `--turbo` | 开关 | 极速模式；本接口 v7、v8.1、v8.2 可用，费用以控制台结算为准 |
| `--draft` | 开关 | 草图模式；本章示例使用 v7，后续增强使用 `/v1/tob/enhance` |
| `--hd` | 开关；仅 v8.1/v8.2 | 原生 2K 档位；并非固定 2048×2048，像素尺寸随比例变化 |
| `--exp` | 0–100；默认 0；v7/v8.1/v8.2 | 实验性美学强度 |
| `--q` | 按上表版本选择 | 不套用其他版本的枚举 |

### 组合约束与示例

- `--fast` 与 `--turbo` 不同时使用；不要同时发送多个相冲突的版本或比例。
- niji 7 不发送 `--q`。
- v7 的 `--draft` 不与 `--tile`、`--oref` 组合。draft 请求不附加 `--q 4`，不据此承诺草图的质量档位。
- v7 的 `--oref` 不与 `--q 4` 组合。
- `--hd` 仅用于 v8.1/v8.2。不要向旧版本追加 HD 参数。
- 不使用独立 JSON 的 `size`、`n`、`response_format` 控制 MJ；结果数量以 `urls` 为准，不假定永远是四张。

以下只是参数组合语法，模型和配置由调用方选择，不自动替换版本、质量或模式。

| 组合 | text 末尾 |
|---|---|
| v7 | `--v 7 --fast --q 1 --s 100 --ar 16:9` |
| v8.1 HD | `--v 8.1 --fast --hd --q 4 --raw --ar 16:9` |
| v7 draft | `--v 7 --fast --draft --ar 1:1` |
| niji 7 | `--niji 7 --fast --s 500 --ar 2:3` |

### 计费规则

MJ 按任务计费，不按返回图片数量直接相乘。操作类型和生成模式会改变费用；一次提交返回多张图片，仍按该任务规则计算，二次操作创建的新任务另行计费。两个可用模型名均按其控制台定价及以下适用倍率计算。

计算方式：**任务费用 = 基础任务价格 × 操作倍率 × Turbo 倍率 × Draft 倍率 × HD 倍率**。基础价格、币种和账户分组折扣以控制台为准；满足多个条件时相乘，不择一取最高值。

| 操作 | 操作倍率 |
|---|---|
| diffusion、reroll | 1× |
| variation、inpaint、remix、edit、upload-paint、retexture | 1.5× |
| upscale，type=0 | 1.5× |
| upscale，type=1 | 2× |
| pan、outpaint、remove-background、enhance | 1× |

| 参数条件 | 附加倍率 | 适用范围 |
|---|---|---|
| Turbo 生效 | 2× | 使用 Turbo 模式的任务；不与 --fast 同时发送 |
| --draft 生效且版本为 v7 | 0.5× | 仅 diffusion |
| --hd 生效且版本为 v8.1 / v8.2 | 1.5× | 仅 diffusion |
| 不满足以上条件 | 1× | 不附加相应倍率 |

例如，v7 普通生成是基础价格的 1×，v7 draft 是 0.5×，v8.1/v8.2 HD 是 1.5×，v8.1/v8.2 HD 加 Turbo 是 3×；前提是该参数组合被接口支持。倍率规则不意味着所有版本与参数都能任意组合。v5 专用 upscale type=2/3 的价格单独查看控制台，不套用 type=0/1。

本规则没有为 `--q` 或 `--ar` 单独设置附加倍率；不能将 q4 直接解读为四倍费用，也不能由倍率推断生成质量。`cost.fastCost`、`cost.feeCost` 是服务返回的费用信息，不直接当作客户币种金额。估算使用以上规则，实际扣费以控制台账单为准；失败任务的结算状态也需查账单，不能仅凭 HTTP 状态推断。

### 任务生命周期

1. POST 提交后保存响应 `id`，作为查询与后续操作的 `jobId`。
2. GET `/v1/tob/job/{jobId}` 查询原任务。`status=1` 为执行中，`2` 为成功，`3` 为失败。
3. 成功时按原始 `urls[]` 下标保存媒体。`audits[]` 相同位置的非空内容为审核限制原因；空 URL 不表示可下载结果。
4. 二次操作提交后返回新的 `id`，继续查询新任务。不可把源任务完成状态当作二次操作的完成状态。

`imageNo` 从 0 开始；只能引用该任务实际返回的图片位置。过滤无效项时保留原始下标，不重新编号。

```json
{
  "id": "TASK_ID",
  "status": 1,
  "urls": [],
  "audits": [],
  "comment": "执行中",
  "cost": {"fastCost": 1, "feeCost": 60},
  "request_id": "REQUEST_ID"
}
```

HTTP 200 只表示本次 HTTP 请求完成，仍须判断任务状态。失败保存并输出 `comment`、`cost` 和完整任务响应（包括存在时的 `reason`、`error_code`、`error_source`、`request_id`），仅写入受控业务日志，不公开鉴权信息或签名 URL。未知状态应保留原响应，不显示为成功。`cost` 为服务返回的任务费用信息，客户结算以控制台账单为准。

### 图片接口

响应示例中的 cost 数值仅用于展示结构，不是价格承诺。版本和参数语法参见[悠船模型参数](https://tob.youchuan.cn/docs/guides/parameters/models)；本章接入域名和鉴权以 YunQi 约定为准，不复制上游机构鉴权头。

下表字段均为请求体字段，`?` 表示可选。`jobId` 为 string，`imageNo`、`type`、`direction`、`mode` 为 integer，`scale` 为 number。二次操作的源任务必须已完成。

| POST 路径 | 用途 | 字段与取值 |
|---|---|---|
| `/v1/tob/diffusion` | 文生图 / URL 参考图 | `model` 为 `mj_imagine` 或 `youchuan-image`；`text` 非空字符串；版本、比例等参数写在 text 中 |
| `/v1/tob/variation` | 变化 | `jobId`、`imageNo`、`type`；0 轻微，1 强烈；`remixPrompt?` |
| `/v1/tob/upscale` | 高清 | `jobId`、`imageNo`、`type`；0 标准，1 创意，2 为 v5_2x，3 为 v5_4x，类型需与源版本匹配 |
| `/v1/tob/reroll` | 重新生成 | `jobId` |
| `/v1/tob/pan` | 定向延展 | `jobId`、`imageNo`、`direction`、`scale`、`remixPrompt?`；direction：0 下、1 右、2 上、3 左；scale：1.1–3 |
| `/v1/tob/outpaint` | 周边扩图 | `jobId`、`imageNo`、`scale`、`remixPrompt?`；scale：1.1–2 |
| `/v1/tob/inpaint` | 区域重绘 | `jobId`、`imageNo`、`mask`、`remixPrompt?` |
| `/v1/tob/remix` | 提示词重塑 | `jobId`、`imageNo`、`remixPrompt`、`mode?`；0 强变化（默认），1 弱变化 |
| `/v1/tob/edit` | 画布编辑 | `jobId`、`imageNo`、`canvas`、`imgPos`、`remixPrompt`、`mask?` |
| `/v1/tob/upload-paint` | URL 图片画布编辑 | `imgUrl`、`mask`、`canvas`、`imgPos`、`remixPrompt` |
| `/v1/tob/retexture` | 材质 / 风格转绘 | `imgUrl`、`remixPrompt`；提示词版本不低于 6.1 |
| `/v1/tob/remove-background` | 移除背景 | `imgUrl`；输入 PNG、JPEG，输出不作为二次生成的源任务 |
| `/v1/tob/enhance` | 草稿增强 | `jobId`、`imageNo`；仅使用 `--draft` 生成的任务 |

`text` 为 1–8192 字符；v8 系列正文长度不超过 1024 字符，不含图片引用和附加参数。版本写在提示词中，例如 `--v 7`，比例写为 `--ar 3:2`。不要把 MJ 版本当成 GPT Images 的 `model` 字段。

高级编辑与转绘结果只支持继续执行高清。原始任务所用版本会影响可用的二次操作；不得把 v5 的放大类型应用到其他版本。`imgUrl` 必须是服务端可直接读取的图片 URL，不是网页、本机路径或需要登录才能下载的链接。

### 蒙版与画布对象

| 字段 | 类型 | 语义 |
|---|---|---|
| `canvas.width` / `canvas.height` | integer | 编辑画布宽高，像素；生成结果可能按模型规格重新缩放 |
| `imgPos.width` / `imgPos.height` | integer | 原图放置到新画布后的宽高，像素 |
| `imgPos.x` / `imgPos.y` | integer | 原图在新画布中的左上角坐标 |
| `mask.areas` | object[] | 多个重绘区域，每项含原图 width、height 与 points |
| `mask.areas[].points` | number[] | 多边形顶点按 x、y 成对排列，原点在左上角 |
| `mask.url` | string | 与原图同尺寸的黑白蒙版 URL，白色区域重绘 |

`mask.areas` 与 `mask.url` 二选一。画布尺寸用于构图与坐标变换，不保证输出图片与 canvas 宽高逐像素相等。这里的黑白蒙版与 GPT Images 的 Alpha 蒙版不同，不要直接混用。生成式编辑不保证未选区域逐像素不变。

### 各操作请求体

将以下 JSON 对象中某个操作的值作为该 POST 接口的请求体，而不是把整个对象一起提交。`SOURCE_TASK_ID`、`DRAFT_TASK_ID`、`IMAGE_URL` 均需替换为自己的实际值。画布和蒙版坐标示例以 1024×1024 原图为基础，输入尺寸改变时同时调整。

```json
{
  "diffusion": {"model": "mj_imagine", "text": "A blue ceramic mug with a yellow square on a white table --v 7"},
  "variation": {"jobId": "SOURCE_TASK_ID", "imageNo": 0, "type": 0},
  "upscale": {"jobId": "SOURCE_TASK_ID", "imageNo": 0, "type": 0},
  "reroll": {"jobId": "SOURCE_TASK_ID"},
  "pan": {"jobId": "SOURCE_TASK_ID", "imageNo": 0, "direction": 1, "scale": 1.5, "remixPrompt": "Extend the white table to the right"},
  "outpaint": {"jobId": "SOURCE_TASK_ID", "imageNo": 0, "scale": 1.5},
  "inpaint": {
    "jobId": "SOURCE_TASK_ID", "imageNo": 0,
    "mask": {"areas": [{"width": 1024, "height": 1024, "points": [270, 260, 850, 260, 850, 930, 270, 930]}]},
    "remixPrompt": "A bright red ceramic mug with a white star, white table, studio photograph"
  },
  "remix": {"jobId": "SOURCE_TASK_ID", "imageNo": 0, "remixPrompt": "A blue mug on a red table --v 7", "mode": 0},
  "edit": {
    "jobId": "SOURCE_TASK_ID", "imageNo": 0,
    "canvas": {"width": 1280, "height": 1024},
    "imgPos": {"width": 1024, "height": 1024, "x": 128, "y": 0},
    "remixPrompt": "Extend the white tabletop around the blue mug"
  },
  "upload-paint": {
    "imgUrl": "IMAGE_URL",
    "mask": {"areas": [{"width": 1024, "height": 1024, "points": [270, 260, 850, 260, 850, 930, 270, 930]}]},
    "canvas": {"width": 1280, "height": 1024},
    "imgPos": {"width": 1024, "height": 1024, "x": 128, "y": 0},
    "remixPrompt": "A bright red ceramic mug with a white star, white table, studio photograph --v 6.1"
  },
  "retexture": {"imgUrl": "IMAGE_URL", "remixPrompt": "Watercolor painting of a blue mug --v 6.1"},
  "remove-background": {"imgUrl": "IMAGE_URL"},
  "enhance": {"jobId": "DRAFT_TASK_ID", "imageNo": 0}
}
```

### 参考图、风格与主体引用

图片 URL 按顺序放在 `text` 开头，再写提示词。这是 MJ 协议定义的图片引用方式，不使用 GPT Images 的 multipart `image[]`，也不接受任意裸 Base64 代替图片 URL。

```json
{"model":"mj_imagine","text":"IMAGE_URL_1 IMAGE_URL_2 A blue mug on a red table --iw 1 --ar 3:2 --v 7"}
```

四种引用方式有不同语义，不能相互替换：

| 引用方式 | text 写法 | 适用范围与权重 |
|---|---|---|
| 普通垫图 | `IMAGE_URL_1 IMAGE_URL_2 描述 --iw 1 --v 7` | 影响内容、构图和色彩；可放多张公开图片，`--iw` 对普通垫图整体生效，不是每张图的索引 |
| 风格参考 | `描述 --sref STYLE_URL_1 STYLE_URL_2 --sw 100 --sv 6 --v 7` | 参考风格，不保证人物或产品身份；`--sw` 0–1000，默认 100；`--sv` 选择风格参考算法，不是主模型版本 |
| 角色参考 | `描述 --cref PERSON_URL --cw 100 --v 6.1` | v6、v6.1、niji 6；`--cw` 0–100，默认 100；低权重偏向脸部，高权重更多保留发型、服装 |
| 万物引用 | `描述 --oref OBJECT_URL --ow 200 --v 7` | 仅 v7；一次一个引用图片；`--ow` 1–1000，默认 100；不与 draft、q4 组合 |

普通垫图的 `--iw`：v6/v6.1/v7/v8.1/v8.2 为 0–3，niji 7 为 0–2；旧版本不要套用这些范围。v7 的 `--sv` 可为 1–6；v6.1/niji 6 为 1–4；v8.1/v8.2 只用 6。不要把 `--sv 6` 写成 `--v 6`。多张普通垫图或风格参考均以空格分隔 URL；角色/万物引用并非可编辑图层或 Photoshop 式像素锁定。

引用图片用 PNG、JPEG、WebP 或 GIF 的 HTTP(S) 直链，不能是 HTML 网页、本机路径、`file://`、裸 Base64 或登录后才可读的资源。图片必须是调用方有权使用的内容。优先使用稳定的公开直链；如果使用签名 URL，完整保留签名参数，并确保在任务读取图片期间有效。客户业务服务接收外部 URL 时应限制协议、域名和内网访问，避免任意 URL 下载造成 SSRF。

### 可恢复的提交与查询示例

依赖：`python -m pip install requests pillow`。将下列代码保存为项目中的 `mj.py`，密钥从 `YOUCHUAN_API_KEY` 读取。请求体放入 UTF-8 JSON 文件，例如 `request.json`。该环境变量装入的是 YunQi 密钥；通用示例的 `YUNQIAI_API_KEY` 不会被本程序自动读取。

```bash
python mj.py submit diffusion request.json
python mj.py resume TASK_ID
```

`submit` 会保存 `mj-task.json` 后开始查询；`resume` 只查询已有任务，不新建收费任务。每个任务单独保存到 `mj-output/TASK_ID/`，避免后续操作覆盖原图。

```python
import argparse
import io
import json
import os
import re
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from pathlib import Path

import requests
from PIL import Image

BASE = "https://www.yunqiai.chat"
MODEL = "mj_imagine"
MODELS = {"mj_imagine", "youchuan-image"}
HEADERS = {
    "Authorization": "Bearer " + os.environ["YOUCHUAN_API_KEY"],
    "Content-Type": "application/json",
    "Accept": "application/json",
}
OPERATIONS = {
    "diffusion", "variation", "upscale", "reroll", "pan", "outpaint",
    "inpaint", "remix", "edit", "upload-paint", "retexture",
    "remove-background", "enhance",
}


def persist(path, value):
    path.write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8")


def parse(response):
    response.encoding = "utf-8"
    try:
        data = response.json()
    except ValueError:
        data = {"body_excerpt": response.text[:1000]}
    if not response.ok or not isinstance(data, dict) or data.get("error"):
        raise RuntimeError(f"HTTP {response.status_code}: {json.dumps(data, ensure_ascii=False)}")
    return data


def delay_for(response):
    value = response.headers.get("Retry-After", "5")
    try:
        return max(1, float(value))
    except ValueError:
        try:
            return max(1, (parsedate_to_datetime(value) - datetime.now(timezone.utc)).total_seconds())
        except (ValueError, TypeError, OverflowError):
            return 5


def poll(job_id, folder, wait_seconds=360):
    deadline = time.monotonic() + wait_seconds
    while time.monotonic() < deadline:
        remaining = deadline - time.monotonic()
        if remaining <= 0:
            break
        try:
            response = requests.get(BASE + "/v1/tob/job/" + job_id, headers=HEADERS,
                                    timeout=(min(10, remaining), min(30, remaining)))
        except (requests.Timeout, requests.ConnectionError):
            time.sleep(min(5, max(0, deadline - time.monotonic())))
            continue
        if response.status_code in {429, 500, 502, 503, 504}:
            time.sleep(min(delay_for(response), max(0, deadline - time.monotonic())))
            continue
        job = parse(response)
        persist(folder / "result.json", job)
        if job.get("id") != job_id:
            raise RuntimeError("Task id mismatch; inspect result.json")
        status = job.get("status")
        if status == 2:
            return job
        if status == 3:
            # Full diagnostics are for private storage, not public logs.
            raise RuntimeError("Task failed: " + json.dumps(job, ensure_ascii=False))
        if status != 1:
            raise RuntimeError("Unknown task status: " + json.dumps(job, ensure_ascii=False))
        time.sleep(min(5, max(0, deadline - time.monotonic())))
    raise TimeoutError("Polling budget exhausted; resume the same task: " + job_id)


def download(job, folder):
    audits = job.get("audits") or []
    saved, errors = [], []
    for index, url in enumerate(job.get("urls") or []):
        if not url or (index < len(audits) and audits[index]):
            errors.append({"index": index, "error": "unavailable or restricted result"})
            continue
        try:
            # Storage requests must not carry the API authorization header.
            response = requests.get(url, timeout=(15, 120))
            response.raise_for_status()
            with Image.open(io.BytesIO(response.content)) as image:
                image.load()
                path = folder / f"mj-imagine-{job['id']}-{index}.png"
                temporary = path.with_suffix(".png.part")
                image.save(temporary, format="PNG")
                temporary.replace(path)
                saved.append({"index": index, "file": str(path), "size": list(image.size)})
        except (requests.RequestException, OSError, ValueError) as exc:
            errors.append({"index": index, "error": type(exc).__name__})
    persist(folder / "downloads.json", {"saved": saved, "errors": errors})
    for item in saved:
        print(item["file"])
    if errors or not saved:
        raise RuntimeError("Incomplete downloads; inspect downloads.json and resume the same job. Do not resubmit.")


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--wait-seconds", type=float, default=360)
    commands = parser.add_subparsers(dest="command", required=True)
    submit = commands.add_parser("submit")
    submit.add_argument("operation", choices=sorted(OPERATIONS))
    submit.add_argument("body_file")
    resume = commands.add_parser("resume")
    resume.add_argument("job_id")
    args = parser.parse_args()
    if args.wait_seconds <= 0:
        raise ValueError("wait-seconds must be positive")
    data = None
    if args.command == "submit":
        body = json.loads(Path(args.body_file).read_text(encoding="utf-8-sig"))
        if args.operation == "diffusion":
            if set(body) - {"model", "text", "callback"}:
                raise ValueError("Diffusion accepts model, text and optional callback; put generation parameters in text")
            body.setdefault("model", MODEL)
            if body["model"] not in MODELS or not isinstance(body.get("text"), str) or not body["text"].strip():
                raise ValueError("Use model=mj_imagine or youchuan-image and a nonempty text string")
        # Submission is never automatically retried, including after a timeout.
        data = parse(requests.post(BASE + "/v1/tob/" + args.operation,
                                  headers=HEADERS, json=body, timeout=(15, 120)))
        job_id = data.get("id")
    else:
        job_id = args.job_id
    if not isinstance(job_id, str) or not re.fullmatch(r"[A-Za-z0-9_-]+", job_id):
        raise ValueError("Missing or invalid task id; inspect submission without resubmitting")
    folder = Path("mj-output") / job_id
    folder.mkdir(parents=True, exist_ok=True)
    if data is not None:
        persist(folder / "submission.json", data)
        persist(Path("mj-task.json"), {"id": job_id})
    print("Task:", job_id, flush=True)
    job = poll(job_id, folder, args.wait_seconds)
    download(job, folder)


if __name__ == "__main__":
    main()
```

图片解码用于避免保存错误响应；业务中仍需核对画面内容是否满足要求。下载失败时从原任务继续查询或下载，不重新提交生成。

### 超时、限流与通知

每隔 5 秒查询，示例默认等待预算为 360 秒；HTTP 请求自身也有独立超时，因此不是严格的程序总运行时限。六分钟是客户端预算，不代表任务会在六分钟内结束或已取消。预算到期保留 id，之后执行 `python mj.py resume TASK_ID`。

| 查询结果 | 客户端处理 |
|---|---|
| `status=1` | 继续查询同一 id |
| `status=2` | 检查 urls/audits，立即下载全部可用图片 |
| `status=3` | 停止查询，保存 comment、cost 与完整响应；不自动新建任务 |
| HTTP 400 | 请求参数错误，停止查询并修正 |
| HTTP 401 / 403 | 检查密钥、权限与任务归属，停止自动轮询 |
| HTTP 404 | 任务不存在或不可访问；核对 id，不持续轮询 |
| HTTP 429 | 按 Retry-After 等待，再查询同一 id |
| HTTP 500 / 502 / 503 / 504 或短暂网络错误 | 预算内延迟重试 GET，不重复 POST |
| 未知 status / 非 JSON 响应 | 保存诊断并停止，不显示为成功 |

提交阶段超时且未拿到 id 时，任务是否创建未知，不自动重复提交，先通过控制台或客服核对原请求。不要自动更换版本重试，避免额外费用和改变客户的生成要求。

### 图片保存与存储

成功后遍历原始 `urls` 数组，立即下载所有可用图片，分别保存为 `mj-imagine-{JobID}-{原始序号}.png`。示例使用 Pillow 实际编码为 PNG，不只是将 JPEG 改后缀；序号从 0 开始，与后续 `imageNo` 对应。单张下载失败仍处理其他图片，并在 `downloads.json` 记录失败位置；之后从原任务恢复下载，不再次生成。

[悠船存储说明](https://tob.youchuan.cn/docs/guides)规定上游生成图片默认保留 30 天。这不是签名 URL 的有效期，也不是 YunQi 的永久存储承诺。不要长期依赖响应 URL；客户需自行保存到受控对象存储。下载时保留 URL 查询参数，但不要携带 API 鉴权头。

需要通知时，生成和编辑接口可传选填 `callback` URL（reroll 除外）。业务接收通知后按任务 id 主动查询确认，以查询结果作为状态依据；回调处理应幂等，不假定通知只到达一次，也不将猜测的签名头作为认证协议。没有公网通知地址时使用上面的查询流程。

## 13. 工具调用与结构化输出

本节分别采用 OpenAI Chat Completions、OpenAI Responses 与 Anthropic Messages 的原生工具字段，三种结构不能混用。先通过 `GET /v1/models` 确认模型可用，再从一个只含单个函数工具的最小请求开始。

工具只由模型提出调用请求，实际执行必须在应用侧完成。Agent 应校验工具名称和参数，再调用本地函数或外部服务；不要直接执行未经校验的命令或 SQL。

### Chat Completions 工具调用

定义工具：

```json
{
  "model": "gpt-5.6-sol",
  "messages": [
    {"role": "user", "content": "上海今天的天气怎么样？"}
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "查询指定城市的天气",
        "parameters": {
          "type": "object",
          "properties": {
            "city": {"type": "string", "description": "城市名称"}
          },
          "required": ["city"],
          "additionalProperties": false
        }
      }
    }
  ],
  "tool_choice": "auto"
}
```

模型提出调用时，读取 `choices[].message.tool_calls[]`。`function.arguments` 是 JSON 字符串，需要解析并校验。

```json
{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_01",
            "type": "function",
            "function": {
              "name": "get_weather",
              "arguments": "{\"city\":\"上海\"}"
            }
          }
        ]
      }
    }
  ]
}
```

执行工具后，把原 assistant 消息和工具结果加入下一次请求。工具结果消息的 `tool_call_id` 必须与调用 ID 一致。

```json
{
  "role": "tool",
  "tool_call_id": "call_01",
  "content": "晴，28°C"
}
```

### Responses 工具调用

```json
{
  "model": "gpt-5.6-sol",
  "input": "查询上海天气",
  "tools": [
    {
      "type": "function",
      "name": "get_weather",
      "description": "查询指定城市的天气",
      "parameters": {
        "type": "object",
        "properties": {
          "city": {"type": "string"}
        },
        "required": ["city"],
        "additionalProperties": false
      }
    }
  ]
}
```

遍历 `output[]` 中全部 `function_call`，校验工具名和 `arguments`，执行后以原 `call_id` 回传。下一轮 `input` 为原始历史 + 完整 `output[]` + 全部 `function_call_output`，并继续发送 `tools`。保留 reasoning 项，不拼造 ID，不依赖 `previous_response_id`。下例是内容结构节选；运行代码必须使用实际响应项。

```json
{
  "model": "gpt-5.6-sol",
  "input": [
    {"role": "user", "content": "查询上海天气"},
    {"type": "function_call", "call_id": "call_01", "name": "get_weather", "arguments": "{\"city\":\"上海\"}"},
    {
      "type": "function_call_output",
      "call_id": "call_01",
      "output": "晴，28°C"
    }
  ]
}
```

### Responses 完整多轮示例

安装 `requests`，设置 `YUNQIAI_API_KEY` 与客户选定的 `YUNQIAI_MODEL`（本示例用于本文 GPT 文本模型）。示例订单查询为本地只读固定数据，可替换为业务查询；工具执行需要权限校验，有副作用的工具还需要确认和业务去重。每轮保留完整响应项，不依赖服务端会话存储。

```python
import json
import os

import requests

url = 'https://www.yunqiai.chat/v1/responses'
headers = {'Authorization': 'Bearer ' + os.environ['YUNQIAI_API_KEY']}
model = os.environ['YUNQIAI_MODEL']
tools = [{
    'type': 'function', 'name': 'lookup_order',
    'description': 'Read an order status by ID.',
    'parameters': {'type': 'object', 'properties': {'order_id': {'type': 'string'}},
                   'required': ['order_id'], 'additionalProperties': False},
    'strict': True,
}]
history = [{'role': 'user', 'content': 'Use lookup_order for order YQ-8264 and tell me its status.'}]

for turn in range(5):
    payload = {'model': model, 'input': history, 'tools': tools,
               'max_output_tokens': 2048, 'reasoning': {'effort': 'low'}}
    if turn == 0:
        payload['tool_choice'] = 'required'
    response = requests.post(url, headers=headers, json=payload, timeout=(15, 180))
    response.encoding = 'utf-8'
    if not response.ok:
        request_id = response.headers.get('x-request-id') or response.headers.get('request-id')
        raise RuntimeError(f'HTTP {response.status_code}; request_id={request_id}; body={response.text[:2000]}')
    data = response.json()
    if data.get('error') or data.get('status') != 'completed':
        detail = {k: data.get(k) for k in ['id', 'status', 'error', 'incomplete_details', 'request_id']}
        raise RuntimeError('Incomplete or failed response; do not execute tools: ' + json.dumps(detail, ensure_ascii=False))
    output = data.get('output', [])
    history.extend(output)
    calls = [item for item in output if item.get('type') == 'function_call']
    if not calls:
        texts = [part['text'] for item in output if item.get('type') == 'message'
                 for part in item.get('content', []) if part.get('type') == 'output_text']
        if not texts:
            raise RuntimeError('No final text; inspect refusal or other output items.')
        print('\n'.join(texts))
        break
    for item in calls:
        args = json.loads(item['arguments'])
        if item['name'] != 'lookup_order' or args != {'order_id': 'YQ-8264'}:
            raise RuntimeError('Unexpected tool or arguments')
        # Replace this read-only fixture with an authorized business lookup.
        result = {'order_id': 'YQ-8264', 'status': 'shipped'}
        history.append({'type': 'function_call_output', 'call_id': item['call_id'],
                        'output': json.dumps(result)})
else:
    raise RuntimeError('Tool round limit exceeded')
```

### Anthropic Messages 工具调用

```json
{
  "model": "claude-sonnet-4-6",
  "max_tokens": 1024,
  "messages": [
    {"role": "user", "content": "查询上海天气"}
  ],
  "tools": [
    {
      "name": "get_weather",
      "description": "查询指定城市的天气",
      "input_schema": {
        "type": "object",
        "properties": {
          "city": {"type": "string"}
        },
        "required": ["city"]
      }
    }
  ]
}
```

模型提出调用时，遍历 `content[]`，找到 `type` 为 `tool_use` 的内容块，读取 `id`、`name` 和 `input`。下一次请求应保留包含该 `tool_use` 的完整 assistant 消息，再追加一条带 `tool_result` 的 `user` 消息；`tool_use_id` 必须与原调用 ID 一致：

```json
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01",
      "content": "晴，28°C"
    }
  ]
}
```

### Chat Completions JSON Schema 输出

需要稳定 JSON 时，使用 `response_format`。应用侧仍应解析并校验最终 JSON。

```json
{
  "model": "gpt-5.5",
  "messages": [
    {"role": "user", "content": "提取姓名和年龄：小明今年 18 岁"}
  ],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "person",
      "schema": {
        "type": "object",
        "properties": {
          "name": {"type": "string"},
          "age": {"type": "integer"}
        },
        "required": ["name", "age"],
        "additionalProperties": false
      }
    }
  }
}
```

## 14. 流式与非流式返回

### SSE 通用解析规则

- Chat Completions、Responses、Anthropic Messages 与开启流式的 GPT Images 使用 `text/event-stream`。
- 按空行切分 SSE 事件；同一事件有多行 `data:` 时先按换行拼接，再解析 JSON。`event:` 是事件名，冒号开头的行是注释或心跳。
- `data:` 后可以有一个空格，也可以没有；不要只匹配 `data: `。HTTP 响应按 UTF-8 解码，不把任意网络分块直接送入 JSON 解析器。
- 一次网络读取可能只有半条事件，也可能包含多条事件，不要把网络分块直接当成 SSE 事件。
- 使用 SDK 时优先遍历 SDK 提供的事件对象；手动解析时仍应按下方各协议的完成标记判断成功。
- 连接在完成标记前断开时，把本次结果视为不完整；保留已经收到的增量，但不要把它当作完整回答。

手动使用 requests 时，先设置 `response.encoding = "utf-8"`，用下面的迭代器读取已完成的事件，再按对应协议解析 JSON 或处理 `[DONE]`。迭代器不发起请求、不自动重连；其参数为已通过 HTTP 状态检查且使用 `stream=True` 打开的响应对象。

```python
def iter_sse_data(response):
    data_lines = []
    for line in response.iter_lines(decode_unicode=True):
        if line == "":
            if data_lines:
                data = "\n".join(data_lines)
                data_lines = []
                if data:
                    yield data
            continue
        if line.startswith(":"):
            continue
        field, _, value = line.partition(":")
        if value.startswith(" "):
            value = value[1:]
        if field == "data":
            data_lines.append(value)
    # A frame without its terminating blank line is incomplete.
```

### Chat Completions 流

每个 JSON 数据块的 `object` 通常为 `chat.completion.chunk`。按 `choices[].index` 分组处理：

| 字段 | 处理方式 |
|---|---|
| `choices[].delta.role` | 初始化该候选的角色 |
| `choices[].delta.content` | 按到达顺序追加文本 |
| `choices[].delta.tool_calls[]` | 先按 `choices[].index` 区分候选，再按工具调用的 `index` 分组；保留 `id` 与函数名，并拼接 `function.arguments` 字符串 |
| `choices[].finish_reason` | 记录该候选的结束原因；不是整个 SSE 的替代终止标记 |
| `data: [DONE]` | Chat Completions 流正常结束 |

```text
data: {"id":"chatcmpl_01","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"你"},"finish_reason":null}]}

data: {"id":"chatcmpl_01","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: [DONE]
```

工具参数可能跨多个 chunk；只有在对应工具调用结束后，才对拼接完成的 `function.arguments` 做 JSON 解析和参数校验。

### Responses 流

Responses 事件在 JSON 的 `type` 中标识类型。常用事件如下：

| `type` | 关键字段与处理 |
|---|---|
| `response.created` | 初始化响应状态，保存响应 ID |
| `response.output_item.added` | 按 `output_index` 登记新的类型化 Item；函数调用需保存 Item `id`、`name` 与回传结果所需的 `call_id` |
| `response.output_text.delta` | 把 `delta` 追加到对应文本内容 |
| `response.function_call_arguments.delta` | 按 `item_id` 或 `output_index` 找到对应函数调用并拼接 `delta`；完成后再解析 JSON |
| `response.output_item.done` | 用完成后的 Item 替换本地聚合版本；只有 `item.type` 为 `function_call` 时才写入工具调用集合 |
| `response.completed` | 正常结束；其中的 `response.output[]` 是最终完整响应 |
| `response.failed`、`response.incomplete`、`error` | 不要标记为成功；保留事件中的错误或不完整原因 |

`response.output_text.delta` 只用于流式聚合；非流式原始 HTTP 响应仍读取类型化的 `output[]`。SDK 的 `output_text` 是完成后聚合文本的便利属性。

Responses 的 `error` 流事件读取事件顶层的 `code`、`message` 与 `param`；如兼容层同时返回嵌套 `error`，可先取嵌套对象，否则读取事件本身。工具执行完成后，使用先前保存的 `call_id` 构造 `function_call_output`。

### Anthropic Messages 流

Anthropic SSE 同时提供 `event:` 名称和 JSON `data.type`。生命周期顺序为：

```text
message_start
content_block_start
content_block_delta
content_block_stop
message_delta
message_stop
```

一个响应可以包含多组 `content_block_start` → `content_block_delta` → `content_block_stop`。按内容块 `index` 聚合：

- `content_block_start.content_block.type: tool_use`：保存该工具调用的 `id` 与 `name`；后续 `tool_result.tool_use_id` 必须使用这个 `id`。
- `delta.type: text_delta`：追加 `delta.text`。
- `delta.type: input_json_delta`：追加 `delta.partial_json`；到 `content_block_stop` 后再把完整字符串解析为工具参数 JSON。
- `message_delta`：读取最终 `stop_reason` 与增量 usage。
- `ping`：心跳事件，可忽略内容但应保持连接。
- `type: error`：流失败，读取 `error.type`、`error.message` 与可用的 `request_id`。
- 收到 `message_stop` 才表示消息流正常结束。

### GPT Images 流

对支持 `stream` 的 Images Generations / Edits 请求：

- 将 `stream` 设为 `true`，`partial_images` 可设为 `0–3`。
- `partial_images: 0` 只返回最终图；大于 `0` 时可返回中间图，但实际中间图数量可能少于请求值。
- 图像事件的 `type` 为 `image_generation.partial_image`，图片序号在 `partial_image_index`，图片 Base64 在 `b64_json`。
- 中间图和最终图使用同一种事件类型。保存每个事件；流正常结束后，把最后收到的图像事件作为最终图。不要等待未定义的 `image_generation.completed` 事件。
- 若连接异常结束或未收到任何图像事件，本次流不完整，不要把某张中间图标记为最终图。

```json
{
  "type": "image_generation.partial_image",
  "partial_image_index": 0,
  "b64_json": "BASE64_IMAGE_DATA"
}
```

### 非流式图片与超时

- Gemini Image 使用 `generateContent`；HTTP 请求完成后遍历 `candidates[].content.parts[].inlineData`。
- GPT Images 使用 `stream: false` 时遍历 `data[]`，逐项处理 `b64_json` 或 `url`。
- 图片生成通常比文本请求耗时更长，客户端可先把超时设置为 `3–5 分钟`，再按实际请求大小调整。这是客户端等待时间，不是服务端完成时限。
- GPT Images / Gemini Image 如果在收到完整响应前超时，调用结果处于未知状态，没有已发布的查询端点确认这类请求；不要自动立即重试。MJ 使用 12B 的异步任务查询，不能套用这条同步接口规则。

## 15. OpenAI SDK

### Python

```python
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["YUNQIAI_API_KEY"],
    base_url="https://www.yunqiai.chat/v1",
    max_retries=0,
)

response = client.chat.completions.create(
    model="gpt-5.6-sol",
    messages=[{"role": "user", "content": "你好"}],
)

print(response.choices[0].message.content)
```

### Node.js

```javascript
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.YUNQIAI_API_KEY,
  maxRetries: 0,
  baseURL: "https://www.yunqiai.chat/v1",
});

const response = await client.chat.completions.create({
  model: "gpt-5.6-sol",
  messages: [{ role: "user", content: "你好" }],
});

console.log(response.choices[0].message.content);
```

## 16. Agent 客户端配置

先备份并合并现有配置，不整文件覆盖，不修改用户的权限规则。示例中的模型仅用于展示字段，主模型、别名和子代理模型由客户确定；不要自动切换型号。对每个使用的型号确认密钥权限。

通用 Python / Node 示例读取 `YUNQIAI_API_KEY`；MJ 专用示例读取 `YOUCHUAN_API_KEY`，同样填入具有相应权限的 YunQi 密钥。PowerShell 当前会话可设置 `$env:YUNQIAI_API_KEY = "YOUR_API_KEY"`；Bash 可设置 `export YUNQIAI_API_KEY="YOUR_API_KEY"`。通过私有环境注入真实密钥，不保留在仓库、共享脚本或 Shell 历史中。cURL 示例使用 Bash 换行与引号；Windows PowerShell 不要直接粘贴 Bash 的反斜杠续行，可运行 Python 示例。

### Codex

`~/.codex/config.toml`：

```toml
model = "gpt-5.6-sol"
model_provider = "yunqi"
model_reasoning_effort = "high"
model_verbosity = "high"
cli_auth_credentials_store = "file"

[model_providers.yunqi]
name = "YunQi AI"
base_url = "https://www.yunqiai.chat/v1"
requires_openai_auth = true
wire_api = "responses"
```

`~/.codex/auth.json`：

```json
{
  "auth_mode": "apikey",
  "OPENAI_API_KEY": "YOUR_API_KEY"
}
```

### Codex 环境变量鉴权（另一种方式）

若需保留已有 `auth.json`，合并以下 provider，密钥由运行进程的 `YUNQIAI_API_KEY` 提供。此方式不要同时设置 `requires_openai_auth = true` 或其他 Bearer token 字段；上面的 auth.json 方式与此方式二选一。

```toml
model = "gpt-5.6-sol"
model_provider = "yunqi"

[model_providers.yunqi]
name = "YunQi AI"
base_url = "https://www.yunqiai.chat/v1"
env_key = "YUNQIAI_API_KEY"
wire_api = "responses"
```

配置字段见 [Codex 配置参考](https://developers.openai.com/codex/config-reference/)。使用 auth.json 方式会切换登录身份，请先备份原有凭据。

### Claude Code

`~/.claude/settings.json`：

```json
{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "env": {
    "ANTHROPIC_BASE_URL": "https://www.yunqiai.chat",
    "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY",
    "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-6",
    "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-8",
    "CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-4-6"
  },
  "model": "claude-sonnet-4-6"
}
```

### CC Switch

在对应客户端标签页添加自定义 YunQi AI 服务商。

| 字段 | Claude Code 标签页 | Codex 标签页 |
|---|---|---|
| API 格式 | Anthropic Messages（原生） | OpenAI Responses |
| Base URL | `https://www.yunqiai.chat` | `https://www.yunqiai.chat/v1` |
| 最终路径 | `/v1/messages` | `/v1/responses` |
| API Key | 控制台创建的 YunQi Key | 控制台创建的 YunQi Key |
| 模型 | 客户选定的 Claude 完整型号 | 客户选定的 GPT 完整型号 |

Claude 认证字段使用 `ANTHROPIC_API_KEY` 或 `ANTHROPIC_AUTH_TOKEN`，只保留一种有效来源，避免旧环境变量覆盖。Codex 的 `model_provider` 与 provider 表名一致，`wire_api = "responses"`。不要把 /v1/messages 填成 Base URL。

保存后启用对应标签页的服务商，退出旧客户端会话并重启。若使用 CC Switch 本地代理接管，配置中的回环地址可能是正常代理入口；确认代理运行，不要同时手工改写它管理的配置。

地址测速、模型列表和生成请求验证不同环节。最终用选定模型发送短消息，再检查流式和工具调用；测速失败本身不能证明模型接口不兼容。

- DNS / TLS / timeout：检查域名拼写、系统时间、DNS、代理和证书链。域名为 `www.yunqiai.chat`。
- 域名失败而 IP 成功：检查域名代理规则、IPv4/IPv6 路由、SNI 与证书。HTTP IP 成功不代表 HTTPS 域名连接正常。
- 401 / 403：检查启用的服务商、鉴权字段冲突、Key 和权限。
- 404：检查重复 /v1、完整路径和模型 ID。
- 429 / 503：读取正文区分额度、限流、分组或渠道问题，保留请求 ID。

不要关闭 TLS 校验、使用 `--insecure` 或将带 Key 的调用降级到 HTTP。HTTPS IP 需要证书也覆盖该 IP。客户端可能使用不同的代理与证书存储，不能仅凭浏览器能访问就断言 API 客户端正常。

字段界面以安装版本为准，参考 [CC Switch 服务商配置](https://github.com/farion1231/cc-switch/blob/main/docs/user-manual/en/2-providers/2.1-add.md)。

### VS Code、WSL、SSH 与容器

配置放在实际运行客户端的环境。Windows 本机的 `~` 为 `%USERPROFILE%`；macOS、Linux、WSL 和 SSH 为该环境的用户目录。WSL 不会自动读取 Windows 的个人配置。

VS Code Remote SSH、WSL 或容器中，先确认扩展运行在本机还是远程，再修改对应环境的 `~/.claude/settings.json` 或 `~/.codex/config.toml`。更新环境变量后完全退出并重启 VS Code 和客户端；集成终端设置的变量不会反向传给已运行的扩展。

Claude Code 在新会话用 `/status` 检查设置来源、`/model` 检查型号。项目设置或组织策略可能覆盖用户设置，JSON 不允许注释和尾逗号。不要为排查连接问题关闭工具权限。Claude 网页版聊天不通过这些本地配置接入。参考 [Claude Code 设置](https://code.claude.com/docs/en/settings)。

### WorkBuddy

| 字段 | 值 |
|---|---|
| 提供商 | `自定义 / Custom` |
| 接口地址 | `https://www.yunqiai.chat/v1/chat/completions` |
| API Key | `YOUR_API_KEY` |
| 模型名称 | `gpt-5.6-sol` 或 `GET /v1/models` 返回的其他文本模型 |
| 高级配置 | 勾选“工具调用”和“自定义协议”；图片输入和推理模式按模型能力开启 |

WorkBuddy 填写完整接口地址，不是 Base URL。

### Cherry Studio、Chatbox 与其他 OpenAI Compatible 客户端

| 字段 | 值 |
|---|---|
| 服务名称 | `YunQi AI` |
| Base URL | `https://www.yunqiai.chat/v1` |
| API Key | `YOUR_API_KEY` |
| 模型 | 客户选定的完整模型 ID |

如果客户端可自动读取模型，使用 `GET /v1/models`；否则手动填写完整模型 ID。

## 17. 错误与重试

### 错误响应结构

先按 HTTP 状态判断成功或失败，再根据所用协议解析错误体。常见 envelope 如下。

OpenAI Compatible、Responses 与 Images：

```json
{
  "error": {
    "message": "The requested model was not found.",
    "type": "invalid_request_error",
    "param": "model",
    "code": "model_not_found"
  }
}
```

Anthropic Messages：

```json
{
  "type": "error",
  "error": {
    "type": "authentication_error",
    "message": "Invalid API key"
  },
  "request_id": "req_01JY7X"
}
```

Gemini Generate Content：

```json
{
  "error": {
    "code": 400,
    "message": "Invalid request.",
    "status": "INVALID_ARGUMENT"
  }
}
```

代码不要只匹配错误文案。OpenAI 风格优先读取 `error.code`、`error.type`、`error.param`、`error.message`；Anthropic 读取顶层 `type`、`error.type`、`error.message`、`request_id`；Gemini 读取 `error.code`、`error.status`、`error.message`。若响应不是 JSON，保留 HTTP 状态、`Content-Type` 和经过长度限制的文本正文。

### Request ID

- OpenAI 风格响应优先读取 `x-request-id` 响应头。
- Anthropic 响应读取 `request-id` 响应头，并兼容错误体中的 `request_id`。
- HTTP 头名称不区分大小写。若上述字段不存在，记录请求时间、模型、方法、路径和 HTTP 状态，不要自行生成一个值冒充服务端 Request ID。

```javascript
function readErrorDetails(response, body) {
  const objectBody = body && typeof body === "object" ? body : {};
  const envelope = objectBody.error && typeof objectBody.error === "object"
    ? objectBody.error
    : objectBody;

  return {
    requestId:
      response.headers.get("x-request-id") ??
      response.headers.get("request-id") ??
      objectBody.request_id ??
      null,
    code: envelope.code ?? envelope.type ?? envelope.status ?? null,
    message: envelope.message ?? response.statusText,
  };
}
```

| HTTP 状态 | 含义 | Agent 处理方式 |
|---:|---|---|
| `400` | 请求错误 | 检查 JSON、必填参数、参数类型、图片尺寸和媒体编码 |
| `401` | 鉴权失败 | 检查 API Key、鉴权头格式和是否混入空格 |
| `403` | 权限、分组或余额不足 | 按正文的 `code` 或 `error.code` 区分原因；`insufficient_user_quota` 表示账号余额不足以预扣费，需补充账号额度；权限错误则检查账号与密钥所属分组。不要自动换 Key、模型、端点或重复提交 |
| `404` | 路径或模型不存在 | 检查 Base URL、接口路径，并重新读取 `GET /v1/models` |
| `429` | 限流或额度不足 | 先读取错误正文；限流按 `Retry-After` 等待，额度不足先处理额度 |
| `500` | 服务错误 | 保存 Request ID；POST 结果可能未知，不自动重放 |
| `502` / `503` | 服务暂时不可用 | 保存请求 ID，核对密钥分组与渠道；不自动重放 POST 或替换型号 |

### 最短排查顺序

1. 调用 `GET /v1/models`，确认 API Key 有效并取得真实可见模型。401/403 时先停止生成调用；密钥未过期不代表所属分组权限仍有效。
2. 用只包含必填字段的最小请求复现。
3. 去掉工具、图片、流式输出和高级采样参数。
4. 记录请求时间、模型、接口、HTTP 状态和 Request ID。
5. 联系支持时不要发送 API Key。

### 重试规则

- `400`、`401`、`403`、`404`：先处理请求、鉴权、权限或额度问题，不要原样重试；403 必须结合错误正文判断，不能直接归类为密钥失效。账号余额与 API Key 的额度上限是两项独立限制，新建 Key 或提高 Key 上限不会增加账号余额。
- `429`：先区分额度不足与限流。额度不足先处理额度；明确限流时遵守 `Retry-After`（秒数或 HTTP 日期），限定总等待时间与尝试次数；不要重复执行业务工具。
- `408`、`5xx`、超时、连接断开：POST 可能已经被服务端接受，不自动重放。记录实际请求 ID 和状态，只有业务接受重复调用与计费风险时才发起新请求。
- 已取得 MJ 任务 id 后，GET 查询可按指数退避重试同一个 id，不重新提交生成。
- GPT Images / Gemini Image 请求可使用 `3–5 分钟` 的客户端超时；结果未知时不自动重发。MJ 的提交与查询分开处理，拿到任务 id 后持续查询原任务，具体见 12B。

## 18. Agent 完成检查

- 已调用 `GET /v1/models`，请求中的模型 ID 与返回值完全一致。
- 已按模型选择正确协议和鉴权请求头。
- Base URL 与完整接口地址没有混填。
- API Key 只保存在环境变量或本机私有配置中。
- 已从最小请求开始，再逐项加入高级参数。
- Base64 使用了对应协议要求的 Data URL 或裸 Base64 写法。
- Responses PDF 使用 `input_file.file_data + filename` 或可访问的 `file_url`；Anthropic PDF 使用内联 `document.source`，不使用文件 ID。
- multipart 图片编辑已上传实际文件，而不是把 Base64 字符串直接放入文件字段。
- Chat Completions 遍历 `choices[]`；Responses 遍历 `output[]`；Messages 遍历 `content[]`；Gemini 遍历 `candidates[].content.parts[]`；Images 遍历 `data[]`。
- 流式请求按对应协议的完成标记结束；完成标记前断线的结果没有被当作完整回答。
- 图片客户端超时已按请求耗时调整；超时后不会自动重复提交。
- 错误日志保留 Request ID，但不包含 API Key、文件正文或图片 Base64。

## 19. 协议参考

- OpenAI Responses API：<https://developers.openai.com/api/reference/resources/responses/methods/create>
- OpenAI Responses 流式输出：<https://developers.openai.com/api/docs/guides/streaming-responses>
- OpenAI 文件输入：<https://developers.openai.com/api/docs/guides/file-inputs>
- OpenAI 图片与视觉输入：<https://developers.openai.com/api/docs/guides/images-vision>
- OpenAI 图片生成指南：<https://developers.openai.com/api/docs/guides/image-generation>
- OpenAI Images Generations：<https://developers.openai.com/api/reference/resources/images/methods/generate>
- OpenAI Images Edits：<https://developers.openai.com/api/reference/resources/images/methods/edit>
- Anthropic Messages API：<https://platform.claude.com/docs/en/api/messages/create>
- Anthropic Messages 流式输出：<https://platform.claude.com/docs/en/build-with-claude/streaming>
- Anthropic 图片输入：<https://platform.claude.com/docs/en/build-with-claude/vision>
- Anthropic PDF 输入：<https://platform.claude.com/docs/en/build-with-claude/pdf-support>
- Gemini Generate Content：<https://ai.google.dev/api/generate-content>
- Gemini 图片理解：<https://ai.google.dev/gemini-api/docs/image-understanding>
- Gemini 视频理解：<https://ai.google.dev/gemini-api/docs/generate-content/video-understanding>
- Gemini 图片生成：<https://ai.google.dev/gemini-api/docs/generate-content/image-generation>
