图片素材脚本改用 concrete model 与 Tiantoken 凭据

- generate-template-samples 与 generate-anthro-cat-illustrations 的 preferredImageModel 改为 gpt-image-2.5-flare-c
- 两个脚本的 base URL / API key / 超时改为只读 TIANTOKEN_BASE_URL、TIANTOKEN_API_KEY、TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS,不再回退 VectorEngine
- 脚本内 URL 构造函数、错误文案与错误字段名从 VectorEngine 改为 provider 中性的 Tiantoken 口径
- SKILL.md 请求契约、环境变量与模型说明同步到 concrete model + Tiantoken,并说明带参考图的编辑走 gpt-image-2.5-sunburst-c
This commit is contained in:
2026-09-21 22:01:44 +08:00
parent 5c58830545
commit 166977baf0
3 changed files with 61 additions and 49 deletions
+21 -13
View File
@@ -1,11 +1,17 @@
---
name: gpt-image-2-apimart
description: Generate or inspect project image assets through this repository's image workflow using the GPT Image 2.5 business model. Use when Codex needs to create puzzle template sample images, reproduce the server-rs image request body, dry-run image prompts, batch-generate local project thumbnails, or debug VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY image-generation configuration without exposing secrets. The directory name is historical.
description: Generate or inspect project image assets through this repository's Tiantoken GPT Image 2.5 workflow. Use when Codex needs to create puzzle template sample images, reproduce the server-rs image request body, dry-run image prompts, batch-generate local project thumbnails, or debug TIANTOKEN_BASE_URL / TIANTOKEN_API_KEY image-generation configuration without exposing secrets. The directory name is historical.
---
# GPT Image 2.5 project image workflow
Use this skill for project-local image asset generation that must match the repository's image request contract. Use the business model identifier `gpt-image-2.5`; provider concrete model routing is owned by `server-rs`, and this client must not perform a cross-model fallback. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
Use this skill for project-local image asset generation that must match the repository's image request contract. Provider routing is owned by `server-rs`; when this skill talks to the provider directly it must send the concrete provider model and the matching provider credentials, because the provider side only accepts concrete models:
- generation (no reference images): `gpt-image-2.5-flare-c` through Tiantoken
- edits (any reference image): `gpt-image-2.5-sunburst-c` through Tiantoken
- nanobanana (`gemini-3.1-flash-image-preview`) stays on VectorEngine
The business model name `gpt-image-2.5` is resolved to a concrete key by `server-rs` at the task boundary and is never sent to a provider directly. This client must not perform a cross-model fallback. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
## Workflow
@@ -24,15 +30,15 @@ Use this skill for project-local image asset generation that must match the repo
```
5. Save final project assets under `public/` or another explicitly requested workspace path.
6. Never print `VECTOR_ENGINE_API_KEY`. Report only whether configuration exists.
6. Never print `TIANTOKEN_API_KEY`. Report only whether configuration exists.
## Request Contract
The repository image path uses:
```text
POST {VECTOR_ENGINE_BASE_URL}/v1/images/generations
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
POST {TIANTOKEN_BASE_URL}/v1/images/generations
Authorization: Bearer {TIANTOKEN_API_KEY}
Content-Type: application/json
```
@@ -40,7 +46,7 @@ Default body:
```json
{
"model": "gpt-image-2.5",
"model": "gpt-image-2.5-flare-c",
"prompt": "<prompt>",
"n": 1,
"size": "1024x1024"
@@ -50,22 +56,22 @@ Default body:
For visual references, use the edit endpoint instead of the create endpoint:
```text
POST {VECTOR_ENGINE_BASE_URL}/v1/images/edits
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
POST {TIANTOKEN_BASE_URL}/v1/images/edits
Authorization: Bearer {TIANTOKEN_API_KEY}
Content-Type: multipart/form-data
```
Multipart fields:
```text
model=gpt-image-2.5
model=gpt-image-2.5-sunburst-c
prompt=<prompt>
n=1
size=1024x1024
image=@reference.png
```
In this repository, calls with no reference images use `POST /v1/images/generations`; calls with any reference image use `POST /v1/images/edits` and pass references as one or more `image` form parts. Both paths send the business model `gpt-image-2.5`; provider routing and retry policy remain server-owned. Match3D container UI generation embeds `public/match3d-background-references/pot-fused-reference.png` into the edit request as an `image` part.
In this repository, calls with no reference images use `POST /v1/images/generations` with `gpt-image-2.5-flare-c`; calls with any reference image use `POST /v1/images/edits` with `gpt-image-2.5-sunburst-c` and pass references as one or more `image` form parts. Server-side calls still submit the business model `gpt-image-2.5` and let `server-rs` resolve the concrete key; provider routing and retry policy remain server-owned, and pricing follows the same reference-image rule (generation with a reference image is charged at the edit tier). Match3D container UI generation embeds `public/match3d-background-references/pot-fused-reference.png` into the edit request as an `image` part.
Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine image generation currently returns synchronously; do not poll APIMart task endpoints.
@@ -75,12 +81,14 @@ Load environment values from process env first, then `.env.secrets.local`, `.env
Required for live generation:
- `VECTOR_ENGINE_BASE_URL`
- `VECTOR_ENGINE_API_KEY`
- `TIANTOKEN_BASE_URL`
- `TIANTOKEN_API_KEY`
Optional:
- `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`
- `TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS`
`VECTOR_ENGINE_*` values no longer serve GPT Image 2.5: those credentials belong to VectorEngine, which only serves nanobanana. Do not fall back from `TIANTOKEN_*` to `VECTOR_ENGINE_*`.
If the key or base URL is missing, stop after dry-run or explain the missing configuration. Do not ask the user to paste the key in chat.
@@ -9,7 +9,9 @@ const skillRoot = path.resolve(__dirname, '..');
const repoRoot = path.resolve(skillRoot, '..', '..', '..');
const defaultOutDir = path.join(repoRoot, 'public', 'anthro-cat-illustrations');
const defaultTimeoutMs = 1000000;
const preferredImageModel = 'gpt-image-2.5';
// GPT Image 2.5 生成任务只接受 concrete provider model;业务模型名 `gpt-image-2.5`
// 由 server-rs 在任务边界解析,脚本直连 provider 时必须自己给出 concrete key。
const preferredImageModel = 'gpt-image-2.5-flare-c';
const prompts = [
{
@@ -100,18 +102,18 @@ function resolveEnv() {
...process.env,
};
return {
baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '')
baseUrl: String(loaded.TIANTOKEN_BASE_URL || '')
.trim()
.replace(/\/+$/u, ''),
apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(),
apiKey: String(loaded.TIANTOKEN_API_KEY || '').trim(),
timeoutMs: Number.parseInt(
String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
String(loaded.TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
10,
),
};
}
function buildVectorEngineImagesGenerationUrl(baseUrl) {
function buildTiantokenImagesGenerationUrl(baseUrl) {
return baseUrl.endsWith('/v1')
? `${baseUrl}/images/generations`
: `${baseUrl}/v1/images/generations`;
@@ -232,22 +234,22 @@ async function fetchJson(url, options, timeoutMs) {
const text = await response.text();
if (!response.ok) {
const error = new Error(
`VectorEngine ${response.status}: ${text.slice(0, 600)}`,
`Tiantoken ${response.status}: ${text.slice(0, 600)}`,
);
error.vectorEngineStatus = response.status;
error.vectorEngineBody = text;
error.providerStatus = response.status;
error.providerBody = text;
throw error;
}
try {
return JSON.parse(text);
} catch (error) {
error.vectorEngineResponseParse = true;
error.vectorEngineBody = text;
error.providerResponseParse = true;
error.providerBody = text;
throw error;
}
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`);
throw new Error(`Tiantoken request timed out after ${timeoutMs}ms`);
}
throw error;
} finally {
@@ -264,7 +266,7 @@ async function requestImagePayload(env, entry) {
size: '1024x1024',
};
const payload = await fetchJson(
buildVectorEngineImagesGenerationUrl(env.baseUrl),
buildTiantokenImagesGenerationUrl(env.baseUrl),
{
method: 'POST',
headers: {
@@ -280,9 +282,9 @@ async function requestImagePayload(env, entry) {
if (extractImageUrls(payload)[0] || base64Image) {
return payload;
}
const error = new Error(`VectorEngine returned no image for ${entry.id}`);
error.vectorEngineResponseParse = true;
error.vectorEngineBody = JSON.stringify(payload).slice(0, 600);
const error = new Error(`Tiantoken returned no image for ${entry.id}`);
error.providerResponseParse = true;
error.providerBody = JSON.stringify(payload).slice(0, 600);
throw error;
}
@@ -326,7 +328,7 @@ async function generateOne(env, entry, outDir) {
const bytes = decodeStrictBase64Image(b64Images[0]);
if (!bytes) {
throw new Error(
`VectorEngine returned invalid base64 image for ${entry.id}`,
`Tiantoken returned invalid base64 image for ${entry.id}`,
);
}
image = {
@@ -334,7 +336,7 @@ async function generateOne(env, entry, outDir) {
extension: inferExtensionFromBytes(bytes),
};
} else {
throw new Error(`VectorEngine returned no image for ${entry.id}`);
throw new Error(`Tiantoken returned no image for ${entry.id}`);
}
mkdirSync(outDir, { recursive: true });
@@ -381,7 +383,7 @@ if (!env.baseUrl || !env.apiKey) {
console.error(
JSON.stringify({
ok: false,
error: 'Missing VECTOR_ENGINE_BASE_URL or VECTOR_ENGINE_API_KEY',
error: 'Missing TIANTOKEN_BASE_URL or TIANTOKEN_API_KEY',
hasBaseUrl: Boolean(env.baseUrl),
hasApiKey: Boolean(env.apiKey),
}),
@@ -18,7 +18,9 @@ const defaultOutDir = path.join(
'puzzle-creation-templates',
);
const defaultTimeoutMs = 1000000;
const preferredImageModel = 'gpt-image-2.5';
// GPT Image 2.5 生成任务只接受 concrete provider model;业务模型名 `gpt-image-2.5`
// 由 server-rs 在任务边界解析,脚本直连 provider 时必须自己给出 concrete key。
const preferredImageModel = 'gpt-image-2.5-flare-c';
const args = new Map();
for (let index = 2; index < process.argv.length; index += 1) {
@@ -70,18 +72,18 @@ function resolveEnv() {
...process.env,
};
return {
baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '')
baseUrl: String(loaded.TIANTOKEN_BASE_URL || '')
.trim()
.replace(/\/+$/u, ''),
apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(),
apiKey: String(loaded.TIANTOKEN_API_KEY || '').trim(),
timeoutMs: Number.parseInt(
String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
String(loaded.TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
10,
),
};
}
function buildVectorEngineImagesGenerationUrl(baseUrl) {
function buildTiantokenImagesGenerationUrl(baseUrl) {
return baseUrl.endsWith('/v1')
? `${baseUrl}/images/generations`
: `${baseUrl}/v1/images/generations`;
@@ -202,22 +204,22 @@ async function fetchJson(url, options, timeoutMs) {
const text = await response.text();
if (!response.ok) {
const error = new Error(
`VectorEngine ${response.status}: ${text.slice(0, 600)}`,
`Tiantoken ${response.status}: ${text.slice(0, 600)}`,
);
error.vectorEngineStatus = response.status;
error.vectorEngineBody = text;
error.providerStatus = response.status;
error.providerBody = text;
throw error;
}
try {
return JSON.parse(text);
} catch (error) {
error.vectorEngineResponseParse = true;
error.vectorEngineBody = text;
error.providerResponseParse = true;
error.providerBody = text;
throw error;
}
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`);
throw new Error(`Tiantoken request timed out after ${timeoutMs}ms`);
}
throw error;
} finally {
@@ -234,7 +236,7 @@ async function requestImagePayload(env, template) {
size: '1024x1024',
};
const payload = await fetchJson(
buildVectorEngineImagesGenerationUrl(env.baseUrl),
buildTiantokenImagesGenerationUrl(env.baseUrl),
{
method: 'POST',
headers: {
@@ -250,9 +252,9 @@ async function requestImagePayload(env, template) {
if (extractImageUrls(payload)[0] || base64Image) {
return payload;
}
const error = new Error(`VectorEngine returned no image for ${template.id}`);
error.vectorEngineResponseParse = true;
error.vectorEngineBody = JSON.stringify(payload).slice(0, 600);
const error = new Error(`Tiantoken returned no image for ${template.id}`);
error.providerResponseParse = true;
error.providerBody = JSON.stringify(payload).slice(0, 600);
throw error;
}
@@ -296,7 +298,7 @@ async function generateOne(env, template, outDir) {
const bytes = decodeStrictBase64Image(b64Images[0]);
if (!bytes) {
throw new Error(
`VectorEngine returned invalid base64 image for ${template.id}`,
`Tiantoken returned invalid base64 image for ${template.id}`,
);
}
image = {
@@ -304,7 +306,7 @@ async function generateOne(env, template, outDir) {
extension: inferExtensionFromBytes(bytes),
};
} else {
throw new Error(`VectorEngine returned no image for ${template.id}`);
throw new Error(`Tiantoken returned no image for ${template.id}`);
}
mkdirSync(outDir, { recursive: true });
@@ -355,7 +357,7 @@ if (!env.baseUrl || !env.apiKey) {
console.error(
JSON.stringify({
ok: false,
error: 'Missing VECTOR_ENGINE_BASE_URL or VECTOR_ENGINE_API_KEY',
error: 'Missing TIANTOKEN_BASE_URL or TIANTOKEN_API_KEY',
hasBaseUrl: Boolean(env.baseUrl),
hasApiKey: Boolean(env.apiKey),
}),