feat: initialize OmniClaw skills registry
This commit is contained in:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.DS_Store
|
||||||
|
._*
|
||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
*.log
|
||||||
|
tmp/
|
||||||
|
|
||||||
8
AGENTS.md
Normal file
8
AGENTS.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Repository Instructions
|
||||||
|
|
||||||
|
- Keep skills self-contained and installable by copying one folder into `$CODEX_HOME/skills`.
|
||||||
|
- Do not commit real credentials, API keys, OAuth tokens, generated images, or private logs.
|
||||||
|
- Run `node tools/validate.mjs` before committing.
|
||||||
|
- Run `node tools/build-registry.mjs` after changing any skill metadata, skill file, reference, script, or API document.
|
||||||
|
- Commit `registry/manifest.json` and `packages/*.tar.gz` because public installers and website pages may consume them directly.
|
||||||
|
|
||||||
94
README.md
Normal file
94
README.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# OmniClaw Skills Registry
|
||||||
|
|
||||||
|
OmniClaw Skills Registry is the source repository for reusable AI-agent skills and API operation guides published by OmniClaw.
|
||||||
|
|
||||||
|
It is designed for three consumers:
|
||||||
|
|
||||||
|
- Humans browsing API documentation and skill usage guides.
|
||||||
|
- Codex or other AI agents installing skills into their local skill directory.
|
||||||
|
- The OmniClaw release website rendering a searchable public catalog from `registry/manifest.json`.
|
||||||
|
|
||||||
|
## Repository Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
skills/
|
||||||
|
<skill-name>/
|
||||||
|
SKILL.md
|
||||||
|
agents/openai.yaml
|
||||||
|
references/
|
||||||
|
scripts/
|
||||||
|
skill.registry.json
|
||||||
|
apis/
|
||||||
|
<product>/
|
||||||
|
<api>.zh.md
|
||||||
|
<api>.en.md
|
||||||
|
registry/
|
||||||
|
manifest.schema.json
|
||||||
|
manifest.json
|
||||||
|
packages/
|
||||||
|
<skill-name>-<version>.tar.gz
|
||||||
|
tools/
|
||||||
|
build-registry.mjs
|
||||||
|
validate.mjs
|
||||||
|
install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install A Skill
|
||||||
|
|
||||||
|
Default install target is `${CODEX_HOME:-$HOME/.codex}/skills`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/tools/install.sh \
|
||||||
|
| bash -s -- sub2api-gpt-image-2
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternative with git clone:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.omniclaw.store/zsb/omniclaw-skills.git
|
||||||
|
cd omniclaw-skills
|
||||||
|
./tools/install.sh sub2api-gpt-image-2
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart Codex after installing new skills.
|
||||||
|
|
||||||
|
## Current Skills
|
||||||
|
|
||||||
|
- `sub2api-gpt-image-2`: Generate, edit, stream, and troubleshoot GPT Image 2 calls through sub2api/OpenAI-compatible APIs.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Validate everything:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/validate.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
Rebuild manifest and skill packages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/build-registry.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
The build script writes:
|
||||||
|
|
||||||
|
- `registry/manifest.json`
|
||||||
|
- `packages/<skill-name>-<version>.tar.gz`
|
||||||
|
|
||||||
|
Commit generated registry and packages so the release website and remote installers can consume stable artifacts directly.
|
||||||
|
|
||||||
|
## Publishing Contract
|
||||||
|
|
||||||
|
Every skill must include:
|
||||||
|
|
||||||
|
- `SKILL.md`
|
||||||
|
- `skill.registry.json`
|
||||||
|
- `agents/openai.yaml`
|
||||||
|
|
||||||
|
Every API guide should provide both:
|
||||||
|
|
||||||
|
- `*.zh.md`
|
||||||
|
- `*.en.md`
|
||||||
|
|
||||||
|
Do not commit real API keys, OAuth tokens, generated images, or private runtime logs.
|
||||||
|
|
||||||
370
apis/sub2api/gpt-image-2.en.md
Normal file
370
apis/sub2api/gpt-image-2.en.md
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
# GPT Image 2 API Guide
|
||||||
|
|
||||||
|
This guide describes how to call `gpt-image-2` through sub2api or any OpenAI-compatible gateway.
|
||||||
|
|
||||||
|
Default examples use:
|
||||||
|
|
||||||
|
```text
|
||||||
|
BASE_URL=https://claude.omniclaw.store/v1
|
||||||
|
API_KEY=<sub2api API key generated from the /keys page>
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not use ChatGPT OAuth tokens from `.codex/auth.json` as API keys.
|
||||||
|
|
||||||
|
## Quick Summary
|
||||||
|
|
||||||
|
- Direct image generation: call `POST /v1/images/generations` with `model: "gpt-image-2"`.
|
||||||
|
- Image editing: call `POST /v1/images/edits` with multipart `image[]` files and an optional `mask`.
|
||||||
|
- Agent/Codex workflows: keep the main model as a text/agent model such as `gpt-5.5`, then call image generation through the Responses API `image_generation` tool.
|
||||||
|
- Do not use `gpt-image-2` as the Codex main model.
|
||||||
|
- `gpt-image-2` normally returns base64 image data at `data[0].b64_json`.
|
||||||
|
- `3840x2160` 4K output works but is high-latency and high-cost; use 180-300 second timeouts for production.
|
||||||
|
|
||||||
|
## Official Capability Summary
|
||||||
|
|
||||||
|
`gpt-image-2` is an image generation and editing model with text input, image input, and image output support.
|
||||||
|
|
||||||
|
Model aliases:
|
||||||
|
|
||||||
|
```text
|
||||||
|
gpt-image-2
|
||||||
|
gpt-image-2-2026-04-21
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported API surfaces:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/v1/images/generations
|
||||||
|
/v1/images/edits
|
||||||
|
/v1/responses # via image_generation tool
|
||||||
|
```
|
||||||
|
|
||||||
|
Official references:
|
||||||
|
|
||||||
|
- https://developers.openai.com/api/docs/models/gpt-image-2
|
||||||
|
- https://developers.openai.com/api/docs/guides/image-generation
|
||||||
|
- https://developers.openai.com/api/reference/resources/images
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export BASE_URL="https://claude.omniclaw.store/v1"
|
||||||
|
export API_KEY="sk-..."
|
||||||
|
```
|
||||||
|
|
||||||
|
JSON requests require:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer $API_KEY
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
For multipart image edits, let `curl -F` or the SDK set `Content-Type`.
|
||||||
|
|
||||||
|
## Image Generation
|
||||||
|
|
||||||
|
### Minimal Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS "$BASE_URL/images/generations" \
|
||||||
|
-H "Authorization: Bearer $API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"model": "gpt-image-2",
|
||||||
|
"prompt": "A compact Apple-style dashboard UI, clean white background",
|
||||||
|
"size": "1024x1024",
|
||||||
|
"quality": "medium",
|
||||||
|
"output_format": "png",
|
||||||
|
"n": 1
|
||||||
|
}' > image.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Decode the response:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jq -r '.data[0].b64_json' image.json | base64 --decode > image.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4K Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS "$BASE_URL/images/generations" \
|
||||||
|
-H "Authorization: Bearer $API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
--max-time 300 \
|
||||||
|
-d '{
|
||||||
|
"model": "gpt-image-2",
|
||||||
|
"prompt": "A modern product poster, cinematic lighting, premium realistic photography",
|
||||||
|
"size": "3840x2160",
|
||||||
|
"quality": "medium",
|
||||||
|
"output_format": "png",
|
||||||
|
"n": 1
|
||||||
|
}' > image-4k.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Production recommendation: first validate prompts with `1024x1024` or `1536x1024`, then upscale the request to `3840x2160`. `4K + high` can be slow and expensive.
|
||||||
|
|
||||||
|
## Generation Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Recommended value | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `model` | string | `gpt-image-2` | Required. The snapshot `gpt-image-2-2026-04-21` is also valid. |
|
||||||
|
| `prompt` | string | detailed natural language | Required. Include subject, environment, camera, style, lighting, and constraints. |
|
||||||
|
| `n` | number | `1` | Number of images. Prefer single-image requests for retry and billing attribution. |
|
||||||
|
| `size` | string | `1024x1024`, `1536x1024`, `3840x2160` | Flexible sizes are supported when they satisfy the model constraints. |
|
||||||
|
| `quality` | string | `low`, `medium`, `high`, `auto` | Use `low` for drafts, `medium` for normal output, `high` for final assets. |
|
||||||
|
| `output_format` | string | `png`, `jpeg`, `webp` | Default is usually `png`; use `jpeg` for latency-sensitive outputs. |
|
||||||
|
| `output_compression` | number | `0-100` | Only applies to `jpeg` and `webp`. |
|
||||||
|
| `background` | string | `auto`, `opaque` | `gpt-image-2` currently does not support `transparent`. |
|
||||||
|
| `moderation` | string | `auto`, `low` | Adjusts filtering level but does not bypass safety policy. |
|
||||||
|
| `stream` | boolean | `false` | Enables SSE image streaming. |
|
||||||
|
| `partial_images` | number | `0-3` | Streaming only; partial images increase output token cost. |
|
||||||
|
| `user` | string | end-user ID | Useful for audit and abuse monitoring. |
|
||||||
|
|
||||||
|
## Size Constraints
|
||||||
|
|
||||||
|
`size` can be `auto` or a valid `widthxheight` value:
|
||||||
|
|
||||||
|
- Maximum edge length is `3840px`.
|
||||||
|
- Width and height must both be multiples of `16px`.
|
||||||
|
- Long edge to short edge ratio must be at most `3:1`.
|
||||||
|
- Total pixels must be between `655,360` and `8,294,400`.
|
||||||
|
|
||||||
|
Common values:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1024x1024
|
||||||
|
1536x1024
|
||||||
|
1024x1536
|
||||||
|
2048x2048
|
||||||
|
2048x1152
|
||||||
|
3840x2160
|
||||||
|
2160x3840
|
||||||
|
auto
|
||||||
|
```
|
||||||
|
|
||||||
|
Treat outputs larger than `2560x1440` as experimental high-pixel workloads with higher latency, higher cost, and higher failure probability.
|
||||||
|
|
||||||
|
## Response Shape
|
||||||
|
|
||||||
|
Typical response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"created": 1770000000,
|
||||||
|
"background": "opaque",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"b64_json": "...",
|
||||||
|
"revised_prompt": "..."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"model": "gpt-image-2",
|
||||||
|
"output_format": "png",
|
||||||
|
"quality": "medium",
|
||||||
|
"size": "1024x1024",
|
||||||
|
"usage": {
|
||||||
|
"input_tokens": 43,
|
||||||
|
"input_tokens_details": {
|
||||||
|
"image_tokens": 0,
|
||||||
|
"text_tokens": 43
|
||||||
|
},
|
||||||
|
"output_tokens": 196,
|
||||||
|
"output_tokens_details": {
|
||||||
|
"image_tokens": 196,
|
||||||
|
"text_tokens": 0
|
||||||
|
},
|
||||||
|
"total_tokens": 239
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Production systems should store:
|
||||||
|
|
||||||
|
- `model`
|
||||||
|
- `size`
|
||||||
|
- `quality`
|
||||||
|
- `output_format`
|
||||||
|
- `usage.total_tokens`
|
||||||
|
- `usage.input_tokens`
|
||||||
|
- `usage.output_tokens`
|
||||||
|
- latency
|
||||||
|
- upstream account, group, user, and key identifiers
|
||||||
|
|
||||||
|
## Image Editing
|
||||||
|
|
||||||
|
### Single-image Edit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS "$BASE_URL/images/edits" \
|
||||||
|
-H "Authorization: Bearer $API_KEY" \
|
||||||
|
-F "model=gpt-image-2" \
|
||||||
|
-F "image[]=@input.png" \
|
||||||
|
-F "prompt=Replace the sofa with a minimalist white lounge chair" \
|
||||||
|
-F "size=1024x1024" \
|
||||||
|
-F "quality=medium" \
|
||||||
|
-F "output_format=png" \
|
||||||
|
> edit.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Masked Local Edit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS "$BASE_URL/images/edits" \
|
||||||
|
-H "Authorization: Bearer $API_KEY" \
|
||||||
|
-F "model=gpt-image-2" \
|
||||||
|
-F "image[]=@input.png" \
|
||||||
|
-F "mask=@mask.png" \
|
||||||
|
-F "prompt=Change only the transparent masked region into a glass button" \
|
||||||
|
-F "size=1024x1024" \
|
||||||
|
-F "quality=medium" \
|
||||||
|
> edit-mask.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Mask requirements:
|
||||||
|
|
||||||
|
- `image` and `mask` must have the same format and dimensions.
|
||||||
|
- Files must be under 50MB.
|
||||||
|
- `mask` must include an alpha channel.
|
||||||
|
- Do not pass `input_fidelity` for `gpt-image-2`; the model processes image inputs at high fidelity by default.
|
||||||
|
|
||||||
|
## Responses API With `image_generation`
|
||||||
|
|
||||||
|
Use this when an agent should reason about the task before generating an image. The main model should be a text/agent model, such as `gpt-5.5`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS "$BASE_URL/responses" \
|
||||||
|
-H "Authorization: Bearer $API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"model": "gpt-5.5",
|
||||||
|
"input": "Generate a clean product poster for an AI proxy service.",
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"type": "image_generation",
|
||||||
|
"quality": "medium",
|
||||||
|
"size": "1536x1024",
|
||||||
|
"output_format": "png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}' > response-image.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Important:
|
||||||
|
|
||||||
|
- `model` is the main reasoning model, not `gpt-image-2`.
|
||||||
|
- The `image_generation` tool performs the image work.
|
||||||
|
- sub2api may inject the image tool for official Codex clients, but application calls should pass it explicitly.
|
||||||
|
|
||||||
|
## Streaming Images
|
||||||
|
|
||||||
|
The Images API supports SSE streaming:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -N "$BASE_URL/images/generations" \
|
||||||
|
-H "Authorization: Bearer $API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"model": "gpt-image-2",
|
||||||
|
"prompt": "A futuristic city skyline at sunrise",
|
||||||
|
"stream": true,
|
||||||
|
"partial_images": 2,
|
||||||
|
"size": "1536x1024",
|
||||||
|
"quality": "medium"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Events:
|
||||||
|
|
||||||
|
```text
|
||||||
|
image_generation.partial_image
|
||||||
|
image_generation.completed
|
||||||
|
```
|
||||||
|
|
||||||
|
`partial_images` can be `0-3`. Each partial image adds output token cost.
|
||||||
|
|
||||||
|
## SDK Examples
|
||||||
|
|
||||||
|
### Node.js
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import fs from "node:fs";
|
||||||
|
import OpenAI from "openai";
|
||||||
|
|
||||||
|
const client = new OpenAI({
|
||||||
|
apiKey: process.env.API_KEY,
|
||||||
|
baseURL: process.env.BASE_URL ?? "https://claude.omniclaw.store/v1",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await client.images.generate({
|
||||||
|
model: "gpt-image-2",
|
||||||
|
prompt: "A premium product poster for an AI service",
|
||||||
|
size: "1536x1024",
|
||||||
|
quality: "medium",
|
||||||
|
output_format: "png",
|
||||||
|
n: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const b64 = result.data?.[0]?.b64_json;
|
||||||
|
if (!b64) throw new Error("No image returned");
|
||||||
|
fs.writeFileSync("image.png", Buffer.from(b64, "base64"));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```py
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
client = OpenAI(
|
||||||
|
api_key=os.environ["API_KEY"],
|
||||||
|
base_url=os.environ.get("BASE_URL", "https://claude.omniclaw.store/v1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = client.images.generate(
|
||||||
|
model="gpt-image-2",
|
||||||
|
prompt="A premium product poster for an AI service",
|
||||||
|
size="1536x1024",
|
||||||
|
quality="medium",
|
||||||
|
output_format="png",
|
||||||
|
n=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
b64 = result.data[0].b64_json
|
||||||
|
with open("image.png", "wb") as f:
|
||||||
|
f.write(base64.b64decode(b64))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Dispatch
|
||||||
|
|
||||||
|
- Routing: prefer plus/team/pro OpenAI OAuth accounts for image workloads.
|
||||||
|
- Timeout: use 120 seconds for normal images and 300 seconds for 4K.
|
||||||
|
- Retry: only retry transient network failures and 502/503/504 with low retry counts.
|
||||||
|
- Concurrency: 4K output produces many image tokens; use low per-account concurrency. Standard 1024 images can use higher concurrency.
|
||||||
|
- Billing: record `usage` and charge based on input and output tokens. 4K can produce far more output tokens than 1024 images.
|
||||||
|
- Latency: use `jpeg` and `quality: low` for drafts or latency-sensitive previews.
|
||||||
|
- Fallback: if `4K/high` fails, retry `4K/medium`; if that still fails, generate `1536x1024/medium` and upscale separately.
|
||||||
|
|
||||||
|
## Common Errors
|
||||||
|
|
||||||
|
| Symptom | Likely cause | Action |
|
||||||
|
|---|---|---|
|
||||||
|
| `401 INVALID_API_KEY` | Key is not a sub2api key or is disabled/deleted | Generate a new key from `/keys` |
|
||||||
|
| `400 invalid_request_error` | Incompatible params such as transparent background or invalid size | Check `size`, `background`, and `quality` |
|
||||||
|
| `429 usage_limit_reached` | Upstream account usage window hit | Switch plus/team/pro account or wait for reset |
|
||||||
|
| `502 Upstream request failed` | Upstream did not return image data, network failed, or content was refused | Inspect server logs, simplify prompt, lower quality or size |
|
||||||
|
| Request takes over 2 minutes | High pixels or complex prompt | Increase timeout, use streaming, or test lower resolution first |
|
||||||
|
| `/v1/models` does not show `gpt-image-2` | Codex/text model list is not the Images API capability list | Call `/v1/images/generations` directly |
|
||||||
|
|
||||||
|
## Safety Boundary
|
||||||
|
|
||||||
|
Filter clearly disallowed content before sending requests, especially:
|
||||||
|
|
||||||
|
- Sexualized minors or young-looking subjects
|
||||||
|
- Non-consensual sexual content, coercion, or sexual violence
|
||||||
|
- Explicit nudity or graphic sexual activity
|
||||||
|
- Illegal, hateful, or extreme violent content
|
||||||
|
|
||||||
|
For safe romantic scenes, explicitly constrain prompts with terms such as adult, non-explicit, no nudity, and fully clothed.
|
||||||
|
|
||||||
365
apis/sub2api/gpt-image-2.zh.md
Normal file
365
apis/sub2api/gpt-image-2.zh.md
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
# GPT Image 2 API 调用文档
|
||||||
|
|
||||||
|
本文档面向 sub2api/OpenAI-compatible 网关调用 `gpt-image-2`。示例默认使用:
|
||||||
|
|
||||||
|
```text
|
||||||
|
BASE_URL=https://claude.omniclaw.store/v1
|
||||||
|
API_KEY=<从 /keys 页面生成的 sub2api key>
|
||||||
|
```
|
||||||
|
|
||||||
|
不要把 `.codex/auth.json` 里的 ChatGPT OAuth token 当 API key 使用。
|
||||||
|
|
||||||
|
## 快速结论
|
||||||
|
|
||||||
|
- 直接生成图片:使用 `POST /v1/images/generations`,`model` 传 `gpt-image-2`。
|
||||||
|
- 编辑图片:使用 `POST /v1/images/edits`,multipart 上传 `image[]`、可选 `mask`。
|
||||||
|
- Agent/Codex 场景:主模型仍用 `gpt-5.5`,通过 Responses API 的 `image_generation` tool 调图像能力;不要把 Codex 主模型设成 `gpt-image-2`。
|
||||||
|
- `gpt-image-2` 返回 base64 图片数据,通常是 `data[0].b64_json`。
|
||||||
|
- `3840x2160` 4K 可用,但属于高像素、长耗时场景;生产调用应设置 180-300 秒超时。
|
||||||
|
|
||||||
|
## 官方能力摘要
|
||||||
|
|
||||||
|
`gpt-image-2` 是图片生成和编辑模型,支持文本输入、图片输入、图片输出。模型别名和快照:
|
||||||
|
|
||||||
|
```text
|
||||||
|
gpt-image-2
|
||||||
|
gpt-image-2-2026-04-21
|
||||||
|
```
|
||||||
|
|
||||||
|
支持端点:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/v1/images/generations
|
||||||
|
/v1/images/edits
|
||||||
|
/v1/responses # 通过 image_generation tool
|
||||||
|
```
|
||||||
|
|
||||||
|
官方参考:
|
||||||
|
|
||||||
|
- https://developers.openai.com/api/docs/models/gpt-image-2
|
||||||
|
- https://developers.openai.com/api/docs/guides/image-generation
|
||||||
|
- https://developers.openai.com/api/reference/resources/images
|
||||||
|
|
||||||
|
## 认证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export BASE_URL="https://claude.omniclaw.store/v1"
|
||||||
|
export API_KEY="sk-..."
|
||||||
|
```
|
||||||
|
|
||||||
|
所有 JSON 请求带:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer $API_KEY
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
multipart 编辑接口由 `curl -F` 或 SDK 自动设置 `Content-Type`。
|
||||||
|
|
||||||
|
## 生成图片
|
||||||
|
|
||||||
|
### 最小请求
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS "$BASE_URL/images/generations" \
|
||||||
|
-H "Authorization: Bearer $API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"model": "gpt-image-2",
|
||||||
|
"prompt": "A compact Apple-style dashboard UI, clean white background",
|
||||||
|
"size": "1024x1024",
|
||||||
|
"quality": "medium",
|
||||||
|
"output_format": "png",
|
||||||
|
"n": 1
|
||||||
|
}' > image.json
|
||||||
|
```
|
||||||
|
|
||||||
|
解码:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jq -r '.data[0].b64_json' image.json | base64 --decode > image.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4K 请求
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS "$BASE_URL/images/generations" \
|
||||||
|
-H "Authorization: Bearer $API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
--max-time 300 \
|
||||||
|
-d '{
|
||||||
|
"model": "gpt-image-2",
|
||||||
|
"prompt": "A modern product poster, cinematic lighting, premium realistic photography",
|
||||||
|
"size": "3840x2160",
|
||||||
|
"quality": "medium",
|
||||||
|
"output_format": "png",
|
||||||
|
"n": 1
|
||||||
|
}' > image-4k.json
|
||||||
|
```
|
||||||
|
|
||||||
|
生产建议:4K + `high` 很慢且成本高。先用 `1024x1024` 或 `1536x1024` 验证提示词,再升到 `3840x2160`。
|
||||||
|
|
||||||
|
## 生成参数
|
||||||
|
|
||||||
|
| 参数 | 类型 | 建议值 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `model` | string | `gpt-image-2` | 必填。也可用快照 `gpt-image-2-2026-04-21`。 |
|
||||||
|
| `prompt` | string | 详细自然语言 | 必填。写清主体、环境、镜头、风格、限制。 |
|
||||||
|
| `n` | number | `1` | 生成数量。生产建议单张并发调度,便于重试和计费。 |
|
||||||
|
| `size` | string | `1024x1024`、`1536x1024`、`3840x2160` | `gpt-image-2` 支持灵活尺寸,见下方尺寸约束。 |
|
||||||
|
| `quality` | string | `low`、`medium`、`high`、`auto` | 草稿用 `low`,常规用 `medium`,最终图用 `high`。 |
|
||||||
|
| `output_format` | string | `png`、`jpeg`、`webp` | 默认 `png`。延迟敏感优先 `jpeg`。 |
|
||||||
|
| `output_compression` | number | `0-100` | 仅 `jpeg`/`webp` 有意义。 |
|
||||||
|
| `background` | string | `auto`、`opaque` | `gpt-image-2` 当前不支持 `transparent`。 |
|
||||||
|
| `moderation` | string | `auto`、`low` | 控制图像生成过滤强度;仍需遵守内容政策。 |
|
||||||
|
| `stream` | boolean | `false` | 开启 SSE 流式图片事件。 |
|
||||||
|
| `partial_images` | number | `0-3` | 流式时返回部分图片;会增加输出 token 成本。 |
|
||||||
|
| `user` | string | 用户 ID | 终端用户标识,便于审计和滥用监控。 |
|
||||||
|
|
||||||
|
## 尺寸约束
|
||||||
|
|
||||||
|
`gpt-image-2` 的 `size` 可以是 `auto`,也可以是满足约束的 `宽x高`:
|
||||||
|
|
||||||
|
- 最大边不超过 `3840px`
|
||||||
|
- 宽和高都必须是 `16px` 的倍数
|
||||||
|
- 长边/短边比例不超过 `3:1`
|
||||||
|
- 总像素在 `655,360` 到 `8,294,400` 之间
|
||||||
|
|
||||||
|
常用尺寸:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1024x1024 # 方图,通常最快
|
||||||
|
1536x1024 # 横图
|
||||||
|
1024x1536 # 竖图
|
||||||
|
2048x2048 # 2K 方图
|
||||||
|
2048x1152 # 2K 横图
|
||||||
|
3840x2160 # 4K 横图
|
||||||
|
2160x3840 # 4K 竖图
|
||||||
|
auto
|
||||||
|
```
|
||||||
|
|
||||||
|
超过 `2560x1440` 的输出通常应按实验性高像素场景处理:高延迟、高成本、失败概率更高。
|
||||||
|
|
||||||
|
## 返回结构
|
||||||
|
|
||||||
|
典型响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"created": 1770000000,
|
||||||
|
"background": "opaque",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"b64_json": "...",
|
||||||
|
"revised_prompt": "..."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"model": "gpt-image-2",
|
||||||
|
"output_format": "png",
|
||||||
|
"quality": "medium",
|
||||||
|
"size": "1024x1024",
|
||||||
|
"usage": {
|
||||||
|
"input_tokens": 43,
|
||||||
|
"input_tokens_details": {
|
||||||
|
"image_tokens": 0,
|
||||||
|
"text_tokens": 43
|
||||||
|
},
|
||||||
|
"output_tokens": 196,
|
||||||
|
"output_tokens_details": {
|
||||||
|
"image_tokens": 196,
|
||||||
|
"text_tokens": 0
|
||||||
|
},
|
||||||
|
"total_tokens": 239
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
业务侧应持久化:
|
||||||
|
|
||||||
|
- `model`
|
||||||
|
- `size`
|
||||||
|
- `quality`
|
||||||
|
- `output_format`
|
||||||
|
- `usage.total_tokens`
|
||||||
|
- `usage.input_tokens`
|
||||||
|
- `usage.output_tokens`
|
||||||
|
- 请求耗时
|
||||||
|
- 上游账号/分组/用户/key
|
||||||
|
|
||||||
|
## 编辑图片
|
||||||
|
|
||||||
|
### 单图编辑
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS "$BASE_URL/images/edits" \
|
||||||
|
-H "Authorization: Bearer $API_KEY" \
|
||||||
|
-F "model=gpt-image-2" \
|
||||||
|
-F "image[]=@input.png" \
|
||||||
|
-F "prompt=Replace the sofa with a minimalist white lounge chair" \
|
||||||
|
-F "size=1024x1024" \
|
||||||
|
-F "quality=medium" \
|
||||||
|
-F "output_format=png" \
|
||||||
|
> edit.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 局部遮罩编辑
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS "$BASE_URL/images/edits" \
|
||||||
|
-H "Authorization: Bearer $API_KEY" \
|
||||||
|
-F "model=gpt-image-2" \
|
||||||
|
-F "image[]=@input.png" \
|
||||||
|
-F "mask=@mask.png" \
|
||||||
|
-F "prompt=Change only the transparent masked region into a glass button" \
|
||||||
|
-F "size=1024x1024" \
|
||||||
|
-F "quality=medium" \
|
||||||
|
> edit-mask.json
|
||||||
|
```
|
||||||
|
|
||||||
|
遮罩要求:
|
||||||
|
|
||||||
|
- `image` 和 `mask` 必须同格式、同尺寸
|
||||||
|
- 文件小于 50MB
|
||||||
|
- `mask` 必须包含 alpha 通道
|
||||||
|
- `gpt-image-2` 不要传 `input_fidelity`;它自动按高保真处理输入图
|
||||||
|
|
||||||
|
## Responses API 调用 image_generation tool
|
||||||
|
|
||||||
|
用于多轮 Agent、让模型先理解需求再调用图片工具。主模型使用文本/Agent 模型,例如 `gpt-5.5`。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS "$BASE_URL/responses" \
|
||||||
|
-H "Authorization: Bearer $API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"model": "gpt-5.5",
|
||||||
|
"input": "Generate a clean product poster for an AI proxy service.",
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"type": "image_generation",
|
||||||
|
"quality": "medium",
|
||||||
|
"size": "1536x1024",
|
||||||
|
"output_format": "png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}' > response-image.json
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:
|
||||||
|
|
||||||
|
- `model` 是主推理模型,不是 `gpt-image-2`
|
||||||
|
- `image_generation` 工具负责图片生成
|
||||||
|
- sub2api 对 Codex 官方客户端请求会注入 `image_generation` 工具提示,但业务调用仍建议显式传 tool
|
||||||
|
|
||||||
|
## 流式图片
|
||||||
|
|
||||||
|
Image API 支持流式生成:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -N "$BASE_URL/images/generations" \
|
||||||
|
-H "Authorization: Bearer $API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"model": "gpt-image-2",
|
||||||
|
"prompt": "A futuristic city skyline at sunrise",
|
||||||
|
"stream": true,
|
||||||
|
"partial_images": 2,
|
||||||
|
"size": "1536x1024",
|
||||||
|
"quality": "medium"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
事件类型:
|
||||||
|
|
||||||
|
```text
|
||||||
|
image_generation.partial_image
|
||||||
|
image_generation.completed
|
||||||
|
```
|
||||||
|
|
||||||
|
`partial_images` 可设 `0-3`。每张 partial image 会额外产生输出 token 成本。
|
||||||
|
|
||||||
|
## SDK 示例
|
||||||
|
|
||||||
|
### Node.js
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import fs from "node:fs";
|
||||||
|
import OpenAI from "openai";
|
||||||
|
|
||||||
|
const client = new OpenAI({
|
||||||
|
apiKey: process.env.API_KEY,
|
||||||
|
baseURL: process.env.BASE_URL ?? "https://claude.omniclaw.store/v1",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await client.images.generate({
|
||||||
|
model: "gpt-image-2",
|
||||||
|
prompt: "A premium product poster for an AI service",
|
||||||
|
size: "1536x1024",
|
||||||
|
quality: "medium",
|
||||||
|
output_format: "png",
|
||||||
|
n: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const b64 = result.data?.[0]?.b64_json;
|
||||||
|
if (!b64) throw new Error("No image returned");
|
||||||
|
fs.writeFileSync("image.png", Buffer.from(b64, "base64"));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```py
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
client = OpenAI(
|
||||||
|
api_key=os.environ["API_KEY"],
|
||||||
|
base_url=os.environ.get("BASE_URL", "https://claude.omniclaw.store/v1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = client.images.generate(
|
||||||
|
model="gpt-image-2",
|
||||||
|
prompt="A premium product poster for an AI service",
|
||||||
|
size="1536x1024",
|
||||||
|
quality="medium",
|
||||||
|
output_format="png",
|
||||||
|
n=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
b64 = result.data[0].b64_json
|
||||||
|
with open("image.png", "wb") as f:
|
||||||
|
f.write(base64.b64decode(b64))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 生产调度建议
|
||||||
|
|
||||||
|
- 路由:图片生成优先使用 plus/team/pro OpenAI OAuth 账号,避免 free 账号能力不足或限流。
|
||||||
|
- 超时:普通图设置 120 秒,4K 设置 300 秒。
|
||||||
|
- 重试:只对网络错误、502/503/504 做有限重试;不要对内容政策拒绝无限重试。
|
||||||
|
- 并发:4K 请求输出 token 高,建议单账号小并发;普通 1024 图可更高并发。
|
||||||
|
- 成本:记录 `usage` 并按 `input_tokens + output_tokens` 计费;4K 输出 token 可能远高于 1024。
|
||||||
|
- 延迟:延迟敏感优先 `jpeg`,草稿用 `quality: low`。
|
||||||
|
- 失败降级:4K/high 失败时降为 4K/medium;仍失败则 1536x1024/medium 先出图,再走放大流程。
|
||||||
|
|
||||||
|
## 常见错误
|
||||||
|
|
||||||
|
| 现象 | 可能原因 | 处理 |
|
||||||
|
|---|---|---|
|
||||||
|
| `401 INVALID_API_KEY` | key 不是 sub2api key,或已删除/停用 | 从 `/keys` 重新生成 key |
|
||||||
|
| `400 invalid_request_error` | 参数不兼容,例如透明背景、尺寸不合法 | 检查 `size`、`background`、`quality` |
|
||||||
|
| `429 usage_limit_reached` | 命中 OpenAI 账号用量窗口 | 切换 plus/team/pro 账号或等待恢复 |
|
||||||
|
| `502 Upstream request failed` | 上游没返回图片、网络断开、内容被拒绝文本化 | 看服务端日志;必要时改提示词/降质量/改尺寸 |
|
||||||
|
| 超过 2 分钟 | 高像素或复杂提示词 | 设置更长超时,使用流式或先低分辨率验证 |
|
||||||
|
| `/v1/models` 不显示 `gpt-image-2` | Codex 主模型列表不等于图片接口能力列表 | 直接调用 `/v1/images/generations` |
|
||||||
|
|
||||||
|
## 安全边界
|
||||||
|
|
||||||
|
业务侧应在请求前过滤明显违规内容,尤其是:
|
||||||
|
|
||||||
|
- 未成年人或年轻化人物的性化内容
|
||||||
|
- 非自愿、胁迫、性暴力场景
|
||||||
|
- 明确裸露或露骨性行为
|
||||||
|
- 违法、仇恨、极端暴力内容
|
||||||
|
|
||||||
|
建议提示词显式写清“成年人、非露骨、无裸露、完全穿着”等约束,降低被上游拒绝或返回非图片文本的概率。
|
||||||
|
|
||||||
BIN
packages/sub2api-gpt-image-2-0.1.0.tar.gz
Normal file
BIN
packages/sub2api-gpt-image-2-0.1.0.tar.gz
Normal file
Binary file not shown.
117
registry/manifest.json
Normal file
117
registry/manifest.json
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0.0",
|
||||||
|
"generated_at": "2026-04-24T08:11:41.942Z",
|
||||||
|
"repository": {
|
||||||
|
"name": "zsb/omniclaw-skills",
|
||||||
|
"git_url": "https://git.omniclaw.store/zsb/omniclaw-skills.git",
|
||||||
|
"raw_base_url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main",
|
||||||
|
"branch": "main"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"name": "sub2api-gpt-image-2",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"title": "Sub2API GPT Image 2",
|
||||||
|
"description": "Generate, edit, stream, and troubleshoot GPT Image 2 calls through sub2api/OpenAI-compatible image APIs.",
|
||||||
|
"path": "skills/sub2api-gpt-image-2",
|
||||||
|
"raw_url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/skills/sub2api-gpt-image-2/SKILL.md",
|
||||||
|
"package": "packages/sub2api-gpt-image-2-0.1.0.tar.gz",
|
||||||
|
"package_url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/packages/sub2api-gpt-image-2-0.1.0.tar.gz",
|
||||||
|
"sha256": "d226c0327193646879e5d9de081a9e29400efa15e194f9c5e00620bd21cceb22",
|
||||||
|
"size_bytes": 5420,
|
||||||
|
"api_docs": [
|
||||||
|
{
|
||||||
|
"id": "sub2api/gpt-image-2.zh",
|
||||||
|
"title": "GPT Image 2 API 调用文档",
|
||||||
|
"product": "sub2api",
|
||||||
|
"language": "zh",
|
||||||
|
"path": "apis/sub2api/gpt-image-2.zh.md",
|
||||||
|
"url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/apis/sub2api/gpt-image-2.zh.md",
|
||||||
|
"tags": [
|
||||||
|
"sub2api",
|
||||||
|
"gpt-image-2",
|
||||||
|
"zh"
|
||||||
|
],
|
||||||
|
"source_urls": [
|
||||||
|
"https://claude.omniclaw.store/v1",
|
||||||
|
"https://developers.openai.com/api/docs/models/gpt-image-2",
|
||||||
|
"https://developers.openai.com/api/docs/guides/image-generation",
|
||||||
|
"https://developers.openai.com/api/reference/resources/images"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sub2api/gpt-image-2.en",
|
||||||
|
"title": "GPT Image 2 API Guide",
|
||||||
|
"product": "sub2api",
|
||||||
|
"language": "en",
|
||||||
|
"path": "apis/sub2api/gpt-image-2.en.md",
|
||||||
|
"url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/apis/sub2api/gpt-image-2.en.md",
|
||||||
|
"tags": [
|
||||||
|
"sub2api",
|
||||||
|
"gpt-image-2",
|
||||||
|
"en"
|
||||||
|
],
|
||||||
|
"source_urls": [
|
||||||
|
"https://claude.omniclaw.store/v1",
|
||||||
|
"https://developers.openai.com/api/docs/models/gpt-image-2",
|
||||||
|
"https://developers.openai.com/api/docs/guides/image-generation",
|
||||||
|
"https://developers.openai.com/api/reference/resources/images"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"sub2api",
|
||||||
|
"openai",
|
||||||
|
"gpt-image-2",
|
||||||
|
"images",
|
||||||
|
"responses",
|
||||||
|
"codex"
|
||||||
|
],
|
||||||
|
"agents": [
|
||||||
|
"codex",
|
||||||
|
"openai-compatible-agent"
|
||||||
|
],
|
||||||
|
"updated_at": "2026-04-24"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"apis": [
|
||||||
|
{
|
||||||
|
"id": "sub2api/gpt-image-2.en",
|
||||||
|
"title": "GPT Image 2 API Guide",
|
||||||
|
"product": "sub2api",
|
||||||
|
"language": "en",
|
||||||
|
"path": "apis/sub2api/gpt-image-2.en.md",
|
||||||
|
"url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/apis/sub2api/gpt-image-2.en.md",
|
||||||
|
"tags": [
|
||||||
|
"sub2api",
|
||||||
|
"gpt-image-2",
|
||||||
|
"en"
|
||||||
|
],
|
||||||
|
"source_urls": [
|
||||||
|
"https://claude.omniclaw.store/v1",
|
||||||
|
"https://developers.openai.com/api/docs/models/gpt-image-2",
|
||||||
|
"https://developers.openai.com/api/docs/guides/image-generation",
|
||||||
|
"https://developers.openai.com/api/reference/resources/images"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sub2api/gpt-image-2.zh",
|
||||||
|
"title": "GPT Image 2 API 调用文档",
|
||||||
|
"product": "sub2api",
|
||||||
|
"language": "zh",
|
||||||
|
"path": "apis/sub2api/gpt-image-2.zh.md",
|
||||||
|
"url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/apis/sub2api/gpt-image-2.zh.md",
|
||||||
|
"tags": [
|
||||||
|
"sub2api",
|
||||||
|
"gpt-image-2",
|
||||||
|
"zh"
|
||||||
|
],
|
||||||
|
"source_urls": [
|
||||||
|
"https://claude.omniclaw.store/v1",
|
||||||
|
"https://developers.openai.com/api/docs/models/gpt-image-2",
|
||||||
|
"https://developers.openai.com/api/docs/guides/image-generation",
|
||||||
|
"https://developers.openai.com/api/reference/resources/images"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
59
registry/manifest.schema.json
Normal file
59
registry/manifest.schema.json
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://release.omniclaw.store/skills/manifest.schema.json",
|
||||||
|
"title": "OmniClaw Skills Registry Manifest",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["schema_version", "generated_at", "repository", "skills", "apis"],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": { "type": "string" },
|
||||||
|
"generated_at": { "type": "string" },
|
||||||
|
"repository": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name", "git_url", "raw_base_url", "branch"],
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"git_url": { "type": "string" },
|
||||||
|
"raw_base_url": { "type": "string" },
|
||||||
|
"branch": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"skills": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name", "version", "title", "description", "path", "package", "sha256"],
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"version": { "type": "string" },
|
||||||
|
"title": { "type": "string" },
|
||||||
|
"description": { "type": "string" },
|
||||||
|
"path": { "type": "string" },
|
||||||
|
"package": { "type": "string" },
|
||||||
|
"sha256": { "type": "string" },
|
||||||
|
"size_bytes": { "type": "number" },
|
||||||
|
"api_docs": { "type": "array" },
|
||||||
|
"tags": { "type": "array", "items": { "type": "string" } },
|
||||||
|
"agents": { "type": "array", "items": { "type": "string" } },
|
||||||
|
"updated_at": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"apis": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "title", "product", "language", "path"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"title": { "type": "string" },
|
||||||
|
"product": { "type": "string" },
|
||||||
|
"language": { "type": "string" },
|
||||||
|
"path": { "type": "string" },
|
||||||
|
"tags": { "type": "array", "items": { "type": "string" } },
|
||||||
|
"source_urls": { "type": "array", "items": { "type": "string" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
85
skills/sub2api-gpt-image-2/SKILL.md
Normal file
85
skills/sub2api-gpt-image-2/SKILL.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
name: sub2api-gpt-image-2
|
||||||
|
description: Use when Codex or another agent needs to call, document, troubleshoot, or generate code for GPT Image 2 through sub2api/OpenAI-compatible image APIs, including /v1/images/generations, /v1/images/edits, Responses image_generation tools, 4K image settings, streaming partial images, base64 decoding, and production error handling.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Sub2API GPT Image 2
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Treat `gpt-image-2` as an image generation/editing model, not a Codex or agent main model.
|
||||||
|
2. Use `/v1/images/generations` for direct text-to-image and `/v1/images/edits` for reference-image or mask edits.
|
||||||
|
3. Use `/v1/responses` with a text/agent model such as `gpt-5.5` plus an `image_generation` tool when an agent should reason before generating images.
|
||||||
|
4. Never print, commit, or hard-code real API keys. Read credentials from `API_KEY` and `BASE_URL`.
|
||||||
|
5. For detailed parameter rules, read `references/api.md`.
|
||||||
|
6. For quick non-streaming generation tests, run `scripts/generate_image.py`.
|
||||||
|
|
||||||
|
## Defaults
|
||||||
|
|
||||||
|
Use these defaults unless the task says otherwise:
|
||||||
|
|
||||||
|
```text
|
||||||
|
BASE_URL=https://claude.omniclaw.store/v1
|
||||||
|
model=gpt-image-2
|
||||||
|
size=1024x1024
|
||||||
|
quality=medium
|
||||||
|
output_format=png
|
||||||
|
n=1
|
||||||
|
timeout=300 for 4K, 120 otherwise
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `3840x2160` only when explicitly requested or when a final 4K asset is needed. For prompt iteration, generate smaller images first.
|
||||||
|
|
||||||
|
## Direct Generation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export API_KEY="sk-..."
|
||||||
|
export BASE_URL="https://claude.omniclaw.store/v1"
|
||||||
|
python3 ~/.codex/skills/sub2api-gpt-image-2/scripts/generate_image.py \
|
||||||
|
--prompt "A premium product poster for an AI service" \
|
||||||
|
--size 1536x1024 \
|
||||||
|
--quality medium \
|
||||||
|
--output image.png
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--dry-run` to inspect payloads without calling the API.
|
||||||
|
|
||||||
|
## Editing
|
||||||
|
|
||||||
|
Use multipart form requests with `image[]=@file.png`; add `mask=@mask.png` only for local masked edits. For `gpt-image-2`, omit `input_fidelity` because it always processes image inputs at high fidelity.
|
||||||
|
|
||||||
|
## Responses Tool Pattern
|
||||||
|
|
||||||
|
For agentic workflows:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model": "gpt-5.5",
|
||||||
|
"input": "Generate a clean product poster for an AI proxy service.",
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"type": "image_generation",
|
||||||
|
"size": "1536x1024",
|
||||||
|
"quality": "medium",
|
||||||
|
"output_format": "png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not set the Responses `model` to `gpt-image-2`.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- `401`: wrong or disabled sub2api key.
|
||||||
|
- `400`: invalid params such as unsupported transparent background or invalid size.
|
||||||
|
- `429`: upstream account usage limit; prefer plus/team/pro accounts.
|
||||||
|
- `502`: upstream did not return image output, network failed, or content was refused; inspect sub2api logs and simplify the prompt.
|
||||||
|
- Long latency: 4K/high can take minutes; use `jpeg`, `quality=low|medium`, or smaller sizes for drafts.
|
||||||
|
|
||||||
|
## Safety
|
||||||
|
|
||||||
|
Reject or rewrite sexualized young-looking subjects, minors, non-consensual sexual content, explicit nudity, and other disallowed content before calling the API.
|
||||||
|
|
||||||
5
skills/sub2api-gpt-image-2/agents/openai.yaml
Normal file
5
skills/sub2api-gpt-image-2/agents/openai.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Sub2API GPT Image 2"
|
||||||
|
short_description: "Generate and debug GPT Image 2 calls."
|
||||||
|
default_prompt: "Use sub2api-gpt-image-2 to generate, edit, stream, or troubleshoot GPT Image 2 API requests."
|
||||||
|
|
||||||
106
skills/sub2api-gpt-image-2/references/api.md
Normal file
106
skills/sub2api-gpt-image-2/references/api.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# GPT Image 2 API Reference For sub2api
|
||||||
|
|
||||||
|
Source links:
|
||||||
|
|
||||||
|
- https://developers.openai.com/api/docs/models/gpt-image-2
|
||||||
|
- https://developers.openai.com/api/docs/guides/image-generation
|
||||||
|
- https://developers.openai.com/api/reference/resources/images
|
||||||
|
- Repository docs: `apis/sub2api/gpt-image-2.zh.md` and `apis/sub2api/gpt-image-2.en.md`
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
```text
|
||||||
|
POST /v1/images/generations
|
||||||
|
POST /v1/images/edits
|
||||||
|
POST /v1/responses # image_generation tool with a text/agent model
|
||||||
|
```
|
||||||
|
|
||||||
|
`/v1/models` may only list Codex/text models in this deployment. That does not mean `/v1/images/generations` cannot accept `gpt-image-2`.
|
||||||
|
|
||||||
|
## Generation Parameters
|
||||||
|
|
||||||
|
| Parameter | Values | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `model` | `gpt-image-2`, `gpt-image-2-2026-04-21` | Required for Images API. |
|
||||||
|
| `prompt` | string | Required. Be concrete about subject, composition, style, lighting, and safety constraints. |
|
||||||
|
| `n` | `1-10` | Prefer `1` for production retries and billing attribution. |
|
||||||
|
| `size` | `auto` or valid `WxH` | Common: `1024x1024`, `1536x1024`, `1024x1536`, `3840x2160`. |
|
||||||
|
| `quality` | `low`, `medium`, `high`, `auto` | Use `low` for drafts, `medium` for normal, `high` for final. |
|
||||||
|
| `output_format` | `png`, `jpeg`, `webp` | `jpeg` is usually faster than `png`. |
|
||||||
|
| `output_compression` | `0-100` | Applies to `jpeg` and `webp`. |
|
||||||
|
| `background` | `auto`, `opaque` | `gpt-image-2` does not support `transparent`. |
|
||||||
|
| `moderation` | `auto`, `low` | Changes filter strictness but does not bypass policy. |
|
||||||
|
| `stream` | boolean | Enables SSE image events. |
|
||||||
|
| `partial_images` | `0-3` | Streaming only; each partial image adds output token cost. |
|
||||||
|
| `user` | string | End-user identifier for abuse monitoring. |
|
||||||
|
|
||||||
|
## Size Constraints
|
||||||
|
|
||||||
|
- Maximum edge length: `3840px`.
|
||||||
|
- Both edges must be multiples of `16px`.
|
||||||
|
- Long edge to short edge ratio must be at most `3:1`.
|
||||||
|
- Total pixels must be between `655360` and `8294400`.
|
||||||
|
- Outputs larger than `2560x1440` should be treated as high-latency experimental outputs.
|
||||||
|
|
||||||
|
## Edit Notes
|
||||||
|
|
||||||
|
- Use multipart form data.
|
||||||
|
- Send reference images as `image[]`.
|
||||||
|
- Optional `mask` must match input image format and dimensions, be under 50MB, and include an alpha channel.
|
||||||
|
- Omit `input_fidelity` for `gpt-image-2`.
|
||||||
|
|
||||||
|
## Response Shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"b64_json": "...",
|
||||||
|
"revised_prompt": "..."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"model": "gpt-image-2",
|
||||||
|
"size": "1024x1024",
|
||||||
|
"quality": "medium",
|
||||||
|
"output_format": "png",
|
||||||
|
"usage": {
|
||||||
|
"input_tokens": 43,
|
||||||
|
"output_tokens": 196,
|
||||||
|
"total_tokens": 239
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Always decode `data[0].b64_json` unless streaming.
|
||||||
|
|
||||||
|
## Production Dispatch
|
||||||
|
|
||||||
|
- Prefer plus/team/pro upstream OpenAI OAuth accounts for image workloads.
|
||||||
|
- Set client timeout to `120s` for normal images and `300s` for 4K.
|
||||||
|
- Retry only transient network/5xx failures with low retry counts.
|
||||||
|
- Do not retry policy or validation errors without changing the prompt or params.
|
||||||
|
- Log `usage`, latency, size, quality, output format, account ID, API key ID, and request ID.
|
||||||
|
|
||||||
|
## Valid curl Template
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS "$BASE_URL/images/generations" \
|
||||||
|
-H "Authorization: Bearer $API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
--max-time 300 \
|
||||||
|
-d '{
|
||||||
|
"model": "gpt-image-2",
|
||||||
|
"prompt": "A premium product poster for an AI service",
|
||||||
|
"size": "1536x1024",
|
||||||
|
"quality": "medium",
|
||||||
|
"output_format": "png",
|
||||||
|
"n": 1
|
||||||
|
}' > image.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Decode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jq -r '.data[0].b64_json' image.json | base64 --decode > image.png
|
||||||
|
```
|
||||||
|
|
||||||
123
skills/sub2api-gpt-image-2/scripts/generate_image.py
Executable file
123
skills/sub2api-gpt-image-2/scripts/generate_image.py
Executable file
@@ -0,0 +1,123 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Generate an image with gpt-image-2 through an OpenAI-compatible /v1/images/generations endpoint."
|
||||||
|
)
|
||||||
|
parser.add_argument("--base-url", default=os.environ.get("BASE_URL", "https://claude.omniclaw.store/v1"))
|
||||||
|
parser.add_argument("--api-key-env", default="API_KEY")
|
||||||
|
parser.add_argument("--prompt", required=True)
|
||||||
|
parser.add_argument("--output", default="image.png")
|
||||||
|
parser.add_argument("--model", default="gpt-image-2")
|
||||||
|
parser.add_argument("--size", default="1024x1024")
|
||||||
|
parser.add_argument("--quality", default="medium", choices=["low", "medium", "high", "auto"])
|
||||||
|
parser.add_argument("--format", default="png", choices=["png", "jpeg", "webp"])
|
||||||
|
parser.add_argument("--compression", type=int, default=None)
|
||||||
|
parser.add_argument("--background", default=None, choices=["auto", "opaque"])
|
||||||
|
parser.add_argument("--moderation", default=None, choices=["auto", "low"])
|
||||||
|
parser.add_argument("--n", type=int, default=1)
|
||||||
|
parser.add_argument("--timeout", type=float, default=300.0)
|
||||||
|
parser.add_argument("--dry-run", action="store_true")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def build_payload(args: argparse.Namespace) -> dict:
|
||||||
|
payload = {
|
||||||
|
"model": args.model,
|
||||||
|
"prompt": args.prompt,
|
||||||
|
"size": args.size,
|
||||||
|
"quality": args.quality,
|
||||||
|
"output_format": args.format,
|
||||||
|
"n": args.n,
|
||||||
|
}
|
||||||
|
if args.compression is not None:
|
||||||
|
payload["output_compression"] = args.compression
|
||||||
|
if args.background:
|
||||||
|
payload["background"] = args.background
|
||||||
|
if args.moderation:
|
||||||
|
payload["moderation"] = args.moderation
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
payload = build_payload(args)
|
||||||
|
url = args.base_url.rstrip("/") + "/images/generations"
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
print(json.dumps({"url": url, "payload": payload}, ensure_ascii=False, indent=2))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
api_key = os.environ.get(args.api_key_env)
|
||||||
|
if not api_key:
|
||||||
|
print(f"Missing API key env: {args.api_key_env}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
data=body,
|
||||||
|
method="POST",
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=args.timeout) as resp:
|
||||||
|
status = resp.status
|
||||||
|
raw = resp.read()
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
raw = exc.read()
|
||||||
|
print(f"HTTP {exc.code}", file=sys.stderr)
|
||||||
|
print(raw.decode("utf-8", "replace")[:4000], file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"request failed: {type(exc).__name__}: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
data = json.loads(raw.decode("utf-8"))
|
||||||
|
if "error" in data:
|
||||||
|
print(json.dumps(data["error"], ensure_ascii=False), file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
items = data.get("data") or []
|
||||||
|
if not items or "b64_json" not in items[0]:
|
||||||
|
print("No b64_json image returned", file=sys.stderr)
|
||||||
|
print(json.dumps(data, ensure_ascii=False)[:4000], file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
image = base64.b64decode(items[0]["b64_json"])
|
||||||
|
with open(args.output, "wb") as f:
|
||||||
|
f.write(image)
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"status": status,
|
||||||
|
"elapsed_seconds": round(elapsed, 3),
|
||||||
|
"output": args.output,
|
||||||
|
"bytes": len(image),
|
||||||
|
"model": data.get("model"),
|
||||||
|
"size": data.get("size"),
|
||||||
|
"quality": data.get("quality"),
|
||||||
|
"output_format": data.get("output_format"),
|
||||||
|
"usage": data.get("usage"),
|
||||||
|
}
|
||||||
|
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
||||||
11
skills/sub2api-gpt-image-2/skill.registry.json
Normal file
11
skills/sub2api-gpt-image-2/skill.registry.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "sub2api-gpt-image-2",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"title": "Sub2API GPT Image 2",
|
||||||
|
"description": "Generate, edit, stream, and troubleshoot GPT Image 2 calls through sub2api/OpenAI-compatible image APIs.",
|
||||||
|
"tags": ["sub2api", "openai", "gpt-image-2", "images", "responses", "codex"],
|
||||||
|
"agents": ["codex", "openai-compatible-agent"],
|
||||||
|
"api_docs": ["sub2api/gpt-image-2.zh", "sub2api/gpt-image-2.en"],
|
||||||
|
"updated_at": "2026-04-24"
|
||||||
|
}
|
||||||
|
|
||||||
157
tools/build-registry.mjs
Executable file
157
tools/build-registry.mjs
Executable file
@@ -0,0 +1,157 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||||
|
import { createReadStream } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
|
||||||
|
const root = path.resolve(new URL("..", import.meta.url).pathname);
|
||||||
|
const skillsDir = path.join(root, "skills");
|
||||||
|
const apisDir = path.join(root, "apis");
|
||||||
|
const packagesDir = path.join(root, "packages");
|
||||||
|
const registryDir = path.join(root, "registry");
|
||||||
|
|
||||||
|
const repository = {
|
||||||
|
name: "zsb/omniclaw-skills",
|
||||||
|
git_url: "https://git.omniclaw.store/zsb/omniclaw-skills.git",
|
||||||
|
raw_base_url: "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main",
|
||||||
|
branch: "main",
|
||||||
|
};
|
||||||
|
|
||||||
|
async function exists(file) {
|
||||||
|
try {
|
||||||
|
await stat(file);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sha256(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const hash = createHash("sha256");
|
||||||
|
const stream = createReadStream(file);
|
||||||
|
stream.on("data", (chunk) => hash.update(chunk));
|
||||||
|
stream.on("error", reject);
|
||||||
|
stream.on("end", () => resolve(hash.digest("hex")));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectApiDocs() {
|
||||||
|
const docs = [];
|
||||||
|
if (!(await exists(apisDir))) {
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
|
const products = await readdir(apisDir, { withFileTypes: true });
|
||||||
|
for (const productEntry of products) {
|
||||||
|
if (!productEntry.isDirectory()) continue;
|
||||||
|
const product = productEntry.name;
|
||||||
|
const productDir = path.join(apisDir, product);
|
||||||
|
const files = await readdir(productDir, { withFileTypes: true });
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file.isFile() || !file.name.endsWith(".md")) continue;
|
||||||
|
const fullPath = path.join(productDir, file.name);
|
||||||
|
const relPath = path.relative(root, fullPath);
|
||||||
|
const text = await readFile(fullPath, "utf8");
|
||||||
|
const title = text.match(/^#\s+(.+)$/m)?.[1]?.trim() || file.name;
|
||||||
|
const language = file.name.endsWith(".zh.md") ? "zh" : file.name.endsWith(".en.md") ? "en" : "unknown";
|
||||||
|
const id = `${product}/${file.name.replace(/\.md$/, "")}`;
|
||||||
|
const sourceUrls = [...text.matchAll(/https:\/\/[^\s)]+/g)].map((m) =>
|
||||||
|
m[0].replace(/[",)}>]+$/g, ""),
|
||||||
|
);
|
||||||
|
docs.push({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
product,
|
||||||
|
language,
|
||||||
|
path: relPath,
|
||||||
|
url: `${repository.raw_base_url}/${relPath}`,
|
||||||
|
tags: Array.from(new Set([product, ...id.split(/[/.]/).filter(Boolean)])),
|
||||||
|
source_urls: Array.from(new Set(sourceUrls)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return docs.sort((a, b) => a.id.localeCompare(b.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildSkillPackage(skillName, version) {
|
||||||
|
await mkdir(packagesDir, { recursive: true });
|
||||||
|
const packageName = `${skillName}-${version}.tar.gz`;
|
||||||
|
const packagePath = path.join(packagesDir, packageName);
|
||||||
|
await rm(packagePath, { force: true });
|
||||||
|
const result = spawnSync("tar", ["-czf", packagePath, "-C", skillsDir, skillName], {
|
||||||
|
cwd: root,
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(`tar failed for ${skillName}`);
|
||||||
|
}
|
||||||
|
const fileStat = await stat(packagePath);
|
||||||
|
return {
|
||||||
|
packageName,
|
||||||
|
packagePath,
|
||||||
|
sizeBytes: fileStat.size,
|
||||||
|
digest: await sha256(packagePath),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectSkills(apiDocs) {
|
||||||
|
const skills = [];
|
||||||
|
if (!(await exists(skillsDir))) {
|
||||||
|
return skills;
|
||||||
|
}
|
||||||
|
const entries = await readdir(skillsDir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
const skillName = entry.name;
|
||||||
|
const skillDir = path.join(skillsDir, skillName);
|
||||||
|
const metadataPath = path.join(skillDir, "skill.registry.json");
|
||||||
|
const skillPath = path.join(skillDir, "SKILL.md");
|
||||||
|
if (!(await exists(metadataPath)) || !(await exists(skillPath))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const metadata = JSON.parse(await readFile(metadataPath, "utf8"));
|
||||||
|
const pkg = await buildSkillPackage(skillName, metadata.version);
|
||||||
|
const relatedDocs = (metadata.api_docs || []).map((docId) => {
|
||||||
|
const doc = apiDocs.find((item) => item.id === docId);
|
||||||
|
return doc || { id: docId };
|
||||||
|
});
|
||||||
|
skills.push({
|
||||||
|
name: skillName,
|
||||||
|
version: metadata.version,
|
||||||
|
title: metadata.title,
|
||||||
|
description: metadata.description,
|
||||||
|
path: `skills/${skillName}`,
|
||||||
|
raw_url: `${repository.raw_base_url}/skills/${skillName}/SKILL.md`,
|
||||||
|
package: `packages/${pkg.packageName}`,
|
||||||
|
package_url: `${repository.raw_base_url}/packages/${pkg.packageName}`,
|
||||||
|
sha256: pkg.digest,
|
||||||
|
size_bytes: pkg.sizeBytes,
|
||||||
|
api_docs: relatedDocs,
|
||||||
|
tags: metadata.tags || [],
|
||||||
|
agents: metadata.agents || ["codex"],
|
||||||
|
updated_at: metadata.updated_at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await mkdir(registryDir, { recursive: true });
|
||||||
|
const apis = await collectApiDocs();
|
||||||
|
const skills = await collectSkills(apis);
|
||||||
|
const manifest = {
|
||||||
|
schema_version: "1.0.0",
|
||||||
|
generated_at: new Date().toISOString(),
|
||||||
|
repository,
|
||||||
|
skills,
|
||||||
|
apis,
|
||||||
|
};
|
||||||
|
await writeFile(path.join(registryDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
||||||
|
console.log(`Built registry: ${skills.length} skills, ${apis.length} API docs`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
99
tools/install.sh
Executable file
99
tools/install.sh
Executable file
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SKILL_NAME="${1:-}"
|
||||||
|
VERSION="${2:-latest}"
|
||||||
|
REPO_URL="${OMNICLAW_SKILLS_REPO:-https://git.omniclaw.store/zsb/omniclaw-skills.git}"
|
||||||
|
RAW_BASE_URL="${OMNICLAW_SKILLS_RAW_BASE_URL:-https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main}"
|
||||||
|
DEST_DIR="${CODEX_HOME:-$HOME/.codex}/skills"
|
||||||
|
|
||||||
|
if [[ -z "$SKILL_NAME" ]]; then
|
||||||
|
echo "Usage: install.sh <skill-name> [version]" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
if ! command -v "$1" >/dev/null 2>&1; then
|
||||||
|
echo "Missing required command: $1" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require_cmd mktemp
|
||||||
|
require_cmd node
|
||||||
|
require_cmd tar
|
||||||
|
require_cmd shasum
|
||||||
|
|
||||||
|
tmp_dir="$(mktemp -d)"
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$tmp_dir"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
manifest="$tmp_dir/manifest.json"
|
||||||
|
if [[ -d "$RAW_BASE_URL" && -f "$RAW_BASE_URL/registry/manifest.json" ]]; then
|
||||||
|
cp "$RAW_BASE_URL/registry/manifest.json" "$manifest"
|
||||||
|
elif command -v curl >/dev/null 2>&1; then
|
||||||
|
curl -fsSL "$RAW_BASE_URL/registry/manifest.json" -o "$manifest" || true
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
wget -qO "$manifest" "$RAW_BASE_URL/registry/manifest.json" || true
|
||||||
|
else
|
||||||
|
: > "$manifest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -s "$manifest" ]]; then
|
||||||
|
require_cmd git
|
||||||
|
git clone --depth 1 "$REPO_URL" "$tmp_dir/repo"
|
||||||
|
if [[ ! -f "$tmp_dir/repo/registry/manifest.json" ]]; then
|
||||||
|
echo "Unable to find registry/manifest.json from $REPO_URL" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cp "$tmp_dir/repo/registry/manifest.json" "$manifest"
|
||||||
|
RAW_BASE_URL="$tmp_dir/repo"
|
||||||
|
fi
|
||||||
|
|
||||||
|
node_script='
|
||||||
|
const fs = require("fs");
|
||||||
|
const manifest = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||||||
|
const name = process.argv[2];
|
||||||
|
const version = process.argv[3];
|
||||||
|
const skill = manifest.skills.find((item) => item.name === name && (version === "latest" || item.version === version));
|
||||||
|
if (!skill) {
|
||||||
|
console.error(`Skill not found: ${name}@${version}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(JSON.stringify(skill));
|
||||||
|
'
|
||||||
|
|
||||||
|
skill_json="$(node -e "$node_script" "$manifest" "$SKILL_NAME" "$VERSION")"
|
||||||
|
package_path="$(printf '%s' "$skill_json" | node -e 'let s="";process.stdin.on("data",c=>s+=c);process.stdin.on("end",()=>console.log(JSON.parse(s).package));')"
|
||||||
|
expected_sha="$(printf '%s' "$skill_json" | node -e 'let s="";process.stdin.on("data",c=>s+=c);process.stdin.on("end",()=>console.log(JSON.parse(s).sha256));')"
|
||||||
|
|
||||||
|
archive="$tmp_dir/skill.tar.gz"
|
||||||
|
if [[ -d "$RAW_BASE_URL" ]]; then
|
||||||
|
cp "$RAW_BASE_URL/$package_path" "$archive"
|
||||||
|
elif command -v curl >/dev/null 2>&1; then
|
||||||
|
curl -fsSL "$RAW_BASE_URL/$package_path" -o "$archive"
|
||||||
|
else
|
||||||
|
wget -qO "$archive" "$RAW_BASE_URL/$package_path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
actual_sha="$(shasum -a 256 "$archive" | awk '{print $1}')"
|
||||||
|
if [[ "$actual_sha" != "$expected_sha" ]]; then
|
||||||
|
echo "Checksum mismatch for $package_path" >&2
|
||||||
|
echo "expected: $expected_sha" >&2
|
||||||
|
echo "actual: $actual_sha" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$DEST_DIR"
|
||||||
|
target="$DEST_DIR/$SKILL_NAME"
|
||||||
|
if [[ -e "$target" ]]; then
|
||||||
|
backup="$target.backup.$(date +%Y%m%d%H%M%S)"
|
||||||
|
mv "$target" "$backup"
|
||||||
|
echo "Existing skill moved to $backup"
|
||||||
|
fi
|
||||||
|
|
||||||
|
tar -xzf "$archive" -C "$DEST_DIR"
|
||||||
|
echo "Installed $SKILL_NAME to $DEST_DIR/$SKILL_NAME"
|
||||||
|
echo "Restart Codex to pick up new skills."
|
||||||
105
tools/validate.mjs
Executable file
105
tools/validate.mjs
Executable file
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { readdir, readFile, stat } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const root = path.resolve(new URL("..", import.meta.url).pathname);
|
||||||
|
const skillsDir = path.join(root, "skills");
|
||||||
|
const apisDir = path.join(root, "apis");
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
async function exists(file) {
|
||||||
|
try {
|
||||||
|
await stat(file);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function check(condition, message) {
|
||||||
|
if (!condition) errors.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSkillFrontmatter(text, file) {
|
||||||
|
const match = text.match(/^---\n([\s\S]*?)\n---/);
|
||||||
|
check(Boolean(match), `${file}: missing YAML frontmatter`);
|
||||||
|
if (!match) return {};
|
||||||
|
const data = {};
|
||||||
|
for (const line of match[1].split("\n")) {
|
||||||
|
const parts = line.split(":");
|
||||||
|
if (parts.length < 2) continue;
|
||||||
|
const key = parts.shift().trim();
|
||||||
|
const value = parts.join(":").trim().replace(/^"|"$/g, "");
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateSkills() {
|
||||||
|
if (!(await exists(skillsDir))) return;
|
||||||
|
const entries = await readdir(skillsDir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
const name = entry.name;
|
||||||
|
const dir = path.join(skillsDir, name);
|
||||||
|
const skillFile = path.join(dir, "SKILL.md");
|
||||||
|
const registryFile = path.join(dir, "skill.registry.json");
|
||||||
|
const agentFile = path.join(dir, "agents", "openai.yaml");
|
||||||
|
check(await exists(skillFile), `${name}: missing SKILL.md`);
|
||||||
|
check(await exists(registryFile), `${name}: missing skill.registry.json`);
|
||||||
|
check(await exists(agentFile), `${name}: missing agents/openai.yaml`);
|
||||||
|
if (!(await exists(skillFile)) || !(await exists(registryFile))) continue;
|
||||||
|
|
||||||
|
const skillText = await readFile(skillFile, "utf8");
|
||||||
|
const frontmatter = parseSkillFrontmatter(skillText, `${name}/SKILL.md`);
|
||||||
|
check(frontmatter.name === name, `${name}: frontmatter name must equal folder name`);
|
||||||
|
check(Boolean(frontmatter.description), `${name}: missing frontmatter description`);
|
||||||
|
|
||||||
|
let metadata;
|
||||||
|
try {
|
||||||
|
metadata = JSON.parse(await readFile(registryFile, "utf8"));
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(`${name}: invalid skill.registry.json: ${error.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
check(metadata.name === name, `${name}: metadata name must equal folder name`);
|
||||||
|
check(Boolean(metadata.version), `${name}: missing version`);
|
||||||
|
check(Boolean(metadata.title), `${name}: missing title`);
|
||||||
|
check(Boolean(metadata.description), `${name}: missing description`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateApiDocs() {
|
||||||
|
if (!(await exists(apisDir))) return;
|
||||||
|
const products = await readdir(apisDir, { withFileTypes: true });
|
||||||
|
for (const productEntry of products) {
|
||||||
|
if (!productEntry.isDirectory()) continue;
|
||||||
|
const productDir = path.join(apisDir, productEntry.name);
|
||||||
|
const files = (await readdir(productDir)).filter((name) => name.endsWith(".md"));
|
||||||
|
const stems = new Map();
|
||||||
|
for (const file of files) {
|
||||||
|
const stem = file.replace(/\.(zh|en)\.md$/, "");
|
||||||
|
if (!stems.has(stem)) stems.set(stem, new Set());
|
||||||
|
if (file.endsWith(".zh.md")) stems.get(stem).add("zh");
|
||||||
|
if (file.endsWith(".en.md")) stems.get(stem).add("en");
|
||||||
|
const text = await readFile(path.join(productDir, file), "utf8");
|
||||||
|
check(/^#\s+.+/m.test(text), `${productEntry.name}/${file}: missing H1 title`);
|
||||||
|
}
|
||||||
|
for (const [stem, languages] of stems) {
|
||||||
|
check(languages.has("zh"), `${productEntry.name}/${stem}: missing zh doc`);
|
||||||
|
check(languages.has("en"), `${productEntry.name}/${stem}: missing en doc`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await validateSkills();
|
||||||
|
await validateApiDocs();
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
console.error(errors.map((item) => `- ${item}`).join("\n"));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Validation passed.");
|
||||||
|
|
||||||
Reference in New Issue
Block a user