Publish 易瞳LLM结构化调用 skill to marketplace

Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources.
Confidence: high
Scope-risk: narrow
Directive: Keep private/internal skills out of the public marketplace and preserve normal incremental market Git history.
Tested: Marketplace validation passed.
This commit is contained in:
KeyInfo Bot
2026-06-18 09:57:10 +08:00
parent 4100886299
commit 44448fe75f
7 changed files with 464 additions and 0 deletions
+12
View File
@@ -291,6 +291,18 @@
"authentication": "ON_INSTALL"
},
"category": "MCP"
},
{
"name": "eapil-llmapi-structured",
"source": {
"source": "local",
"path": "./plugins/codex/plugins/eapil-llmapi-structured"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "开发工具"
}
]
}
@@ -291,6 +291,18 @@
"authentication": "ON_INSTALL"
},
"category": "MCP"
},
{
"name": "eapil-llmapi-structured",
"source": {
"source": "local",
"path": "./plugins/eapil-llmapi-structured"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "开发工具"
}
]
}
@@ -0,0 +1,39 @@
{
"name": "eapil-llmapi-structured",
"version": "0.1.0",
"description": "指导AI以生成结构化 JSON 输出的指南,内容涵盖视觉标注、严格的 JSON 架构要求、推理耗时分析、令牌/延迟诊断以及网关故障排除等主题。适用于在构建、调试等类似兼容 OpenAI 的结构化输出请求时使用。",
"author": {
"name": "EAPIL",
"url": "https://git.playones.com/arechen/EapilSkillMarket"
},
"homepage": "https://git.playones.com/arechen/EapilSkillMarket",
"repository": "https://git.playones.com/arechen/EapilSkillMarket",
"license": "Proprietary",
"keywords": [
"eapil",
"codex-skill",
"eapil-llmapi-structured"
],
"skills": "./skills/",
"interface": {
"displayName": "易瞳LLM结构化调用",
"shortDescription": "指导AI以生成结构化 JSON 输出的指南,内容涵盖视觉标注、严格的 JSON 架构要求、推理耗时分析、令牌/延迟诊断以及网关故障排除等主题。适用于在构建、调试等类似兼容 OpenAI 的结构化输出请求时使用。",
"longDescription": "指导AI以生成结构化 JSON 输出的指南,内容涵盖视觉标注、严格的 JSON 架构要求、推理耗时分析、令牌/延迟诊断以及网关故障排除等主题。适用于在构建、调试等类似兼容 OpenAI 的结构化输出请求时使用。",
"developerName": "EAPIL",
"category": "开发工具",
"capabilities": [
"Read",
"Write"
],
"defaultPrompt": [
"使用 易瞳LLM结构化调用 帮我完成这个任务。"
],
"websiteURL": "https://git.playones.com/arechen/EapilSkillMarket",
"privacyPolicyURL": "https://git.playones.com/arechen/EapilSkillMarket",
"termsOfServiceURL": "https://git.playones.com/arechen/EapilSkillMarket",
"brandColor": "#2563EB",
"screenshots": [],
"composerIcon": "./assets/market-icon.svg",
"logo": "./assets/market-icon.svg"
}
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="360" viewBox="0 0 32 32"><title>易瞳LLM结构化调用</title><g fill="none"><path fill="#d9d9d9" d="M9.472 28.531L17.68 3H21l.006.016A4 4 0 0 1 24.117 5l5.485 9.5a3 3 0 0 1 0 3l-5.773 10A3 3 0 0 1 21.23 29H11l-.004-.016a4 4 0 0 1-1.524-.453"/><path fill="url(#SVGtIZlwdLy)" d="M9.472 28.531L17.68 3H21l.006.016A4 4 0 0 1 24.117 5l5.485 9.5a3 3 0 0 1 0 3l-5.773 10A3 3 0 0 1 21.23 29H11l-.004-.016a4 4 0 0 1-1.524-.453"/><path fill="url(#SVGS9366cPJ)" fill-opacity=".5" d="M9.472 28.531L17.68 3H21l.006.016A4 4 0 0 1 24.117 5l5.485 9.5a3 3 0 0 1 0 3l-5.773 10A3 3 0 0 1 21.23 29H11l-.004-.016a4 4 0 0 1-1.524-.453"/><path fill="url(#SVGLyzYJd2y)" d="m14 26.5l4-21l.043-.01A3 3 0 0 1 21 3H10.774a3 3 0 0 0-2.599 1.5l-5.773 10a3 3 0 0 0 0 3L7.887 27c.285.494.667.913 1.114 1.237a3 3 0 0 0 4.952-1.706z"/><path fill="url(#SVGTHQSMcNS)" fill-opacity=".4" d="m14 26.5l4-21l.043-.01A3 3 0 0 1 21 3H10.774a3 3 0 0 0-2.599 1.5l-5.773 10a3 3 0 0 0 0 3L7.887 27c.285.494.667.913 1.114 1.237a3 3 0 0 0 4.952-1.706z"/><defs><radialGradient id="SVGtIZlwdLy" cx="0" cy="0" r="1" gradientTransform="rotate(-87.021 26.23 10.402)scale(37.937 42.6996)" gradientUnits="userSpaceOnUse"><stop stop-color="#ffc470"/><stop offset=".251" stop-color="#ff835c"/><stop offset=".584" stop-color="#f24a9d"/><stop offset=".871" stop-color="#b339f0"/><stop offset="1" stop-color="#c354ff"/></radialGradient><radialGradient id="SVGS9366cPJ" cx="0" cy="0" r="1" gradientTransform="rotate(214.973 10.84 10.875)scale(27.8693 26.4973)" gradientUnits="userSpaceOnUse"><stop offset=".709" stop-color="#ffb357" stop-opacity="0"/><stop offset=".942" stop-color="#ffb357"/></radialGradient><radialGradient id="SVGLyzYJd2y" cx="0" cy="0" r="1" gradientTransform="rotate(201.076 16.049 10.671)scale(35.1186 31.3225)" gradientUnits="userSpaceOnUse"><stop offset=".222" stop-color="#4e46e2"/><stop offset=".578" stop-color="#625df6"/><stop offset=".955" stop-color="#e37dff"/></radialGradient><linearGradient id="SVGTHQSMcNS" x1="7.792" x2="16.073" y1="13.771" y2="15.597" gradientUnits="userSpaceOnUse"><stop stop-color="#7563f7" stop-opacity="0"/><stop offset=".986" stop-color="#4916ae"/></linearGradient></defs></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,175 @@
---
name: eapil-llmapi-structured
description: Guidance for EAPIL LLM API/OpenAI-compatible GPT-5.5 structured JSON output calls through /v1/responses or /v1/chat/completions, especially vision labeling, strict JSON schema, reasoning_effort, token/latency diagnostics, and gateway troubleshooting. Use when building, debugging, or reviewing structured-output requests to EAPIL LLM API relay domains such as api-n-cd.playones.com or similar OpenAI-compatible relays.
---
# EAPIL LLMAPI Structured Output
Use this skill when implementing or debugging structured JSON calls against the EAPIL LLM API OpenAI-compatible gateway.
## Default Choice
Prefer `/v1/responses` with strict JSON schema for GPT-5.5 teacher labeling.
Keep `/v1/chat/completions` as a fallback for compatibility testing, not the default, unless live smoke tests prove it is cheaper and faster for the same schema.
## Known Good Responses Shape
Use this shape for low-latency structured output:
```json
{
"model": "gpt-5.5",
"input": [
{
"role": "user",
"content": [
{ "type": "input_text", "text": "只输出一个 JSON object,不要 markdown。" },
{ "type": "input_image", "image_url": "data:image/jpeg;base64,...", "detail": "low" }
]
}
],
"text": {
"format": {
"type": "json_schema",
"name": "task_schema_name",
"strict": true,
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"ok": { "type": "boolean" },
"score": { "type": "number", "minimum": 0, "maximum": 1 },
"label": { "type": "string", "maxLength": 64 }
},
"required": ["ok", "score", "label"]
}
}
},
"reasoning": { "effort": "none" },
"truncation": "disabled",
"max_output_tokens": 700
}
```
Call `POST {base_url}/responses`, where `base_url` may already include `/v1`.
## Chat Fallback Shape
Use this only for fallback comparison:
```json
{
"model": "gpt-5.5",
"messages": [
{
"role": "user",
"content": [
{ "type": "text", "text": "只输出一个 JSON object,不要 markdown。" },
{ "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,...", "detail": "low" } }
]
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "task_schema_name",
"strict": true,
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"ok": { "type": "boolean" },
"score": { "type": "number", "minimum": 0, "maximum": 1 },
"label": { "type": "string", "maxLength": 64 }
},
"required": ["ok", "score", "label"]
}
}
},
"reasoning_effort": "none",
"temperature": 0,
"max_tokens": 700
}
```
Call `POST {base_url}/chat/completions`.
## Schema Rules
- Keep schemas compact. Prefer arrays of present tags over objects containing every boolean flag.
- Use `additionalProperties: false` and `required` for every field in strict schemas.
- Bound free text with `maxLength`; keep notes short.
- Use enum arrays for tags:
```json
{
"negative_tags": {
"type": "array",
"maxItems": 8,
"items": {
"type": "string",
"enum": ["empty_frame", "screen_distraction", "subject_too_small"]
}
}
}
```
## Prompt Rules
- State that the model must output only one JSON object, no markdown and no explanation.
- If human decisions are already fixed, state that the model must not change them.
- For visual labeling, require all camera IDs to be present.
- Tell the model to list only actually present negative tags; use `[]` when none are visible.
- Avoid asking for long rationales. Use short notes for human audit only.
## Token And Latency Diagnostics
First compare visible output with usage:
- `raw_text_chars` should roughly match a small structured JSON response.
- `usage.output_tokens_details.reasoning_tokens` should be `0` when reasoning effort is `none`.
- Responses output should not contain an `output` item with `type: "reasoning"` or `encrypted_content`.
If `output_tokens` is thousands while visible JSON is small:
1. Check EAPIL gateway settings for forced reasoning overrides such as XHIGH.
2. Re-test with `/v1/responses`, strict schema, and `reasoning: {"effort":"none"}`.
3. Inspect whether `encrypted_content` appears in the raw response.
4. Do not blame the prompt until gateway forced reasoning is ruled out.
Known good reference from this project after gateway fix:
```text
endpoint: /v1/responses
model: gpt-5.5
reasoning: none
input_tokens: 1704
output_tokens: 230
reasoning_tokens: 0
visible JSON: about 786 chars
elapsed: about 8s
raw response: no encrypted_content
```
## Operational Rules
- Start teacher-labeling concurrency at 1-3. Raise only after observing stable latency and no 429/5xx bursts.
- Persist job status so interrupted labeling can resume from pending/error jobs.
- Log request mode, reasoning effort, usage, elapsed seconds, visible JSON length, parse status, and whether hidden reasoning exists.
- Keep real API keys out of logs and saved request artifacts.
## Smoke Test Script
Use `scripts/smoke_structured_output.py` to verify gateway behavior before large labeling runs:
```bash
python /root/.codex/skills/eapil-llmapi-structured/scripts/smoke_structured_output.py \
--base-url https://api-n-cd.playones.com/v1 \
--model gpt-5.5 \
--endpoint responses \
--reasoning-effort none \
--api-key-env OPENAI_API_KEY
```
Add `--image /path/to/frame.jpg` one or more times for vision tests. Add `--dry-run` to print the request shape without calling the API.
@@ -0,0 +1,4 @@
interface:
display_name: "EAPIL LLMAPI Structured"
short_description: "EAPIL LLM API structured JSON guide"
default_prompt: "Use $eapil-llmapi-structured to build or debug an EAPIL LLM API structured-output request."
@@ -0,0 +1,221 @@
#!/usr/bin/env python3
"""Smoke-test EAPIL/OpenAI-compatible structured output behavior."""
from __future__ import annotations
import argparse
import base64
import json
import mimetypes
import os
import sys
import time
from pathlib import Path
from typing import Any
from urllib import request
SCHEMA: dict[str, Any] = {
"type": "object",
"additionalProperties": False,
"properties": {
"ok": {"type": "boolean"},
"label": {"type": "string", "maxLength": 64},
"score": {"type": "number", "minimum": 0, "maximum": 1},
},
"required": ["ok", "label", "score"],
}
def data_url(path: Path) -> str:
mime = mimetypes.guess_type(path.name)[0] or "image/jpeg"
return f"data:{mime};base64,{base64.b64encode(path.read_bytes()).decode('ascii')}"
def endpoint_url(base_url: str, endpoint: str) -> str:
stripped = base_url.rstrip("/")
if stripped.endswith("/v1"):
return f"{stripped}/{endpoint}"
return f"{stripped}/v1/{endpoint}"
def responses_payload(args: argparse.Namespace) -> dict[str, Any]:
content: list[dict[str, Any]] = [
{
"type": "input_text",
"text": (
"只输出一个 JSON object,不要 markdown。"
"判断输入是否可用于结构化输出 smoke test。"
),
}
]
for image in args.image:
content.append(
{
"type": "input_image",
"image_url": data_url(Path(image)),
"detail": args.image_detail,
}
)
payload: dict[str, Any] = {
"model": args.model,
"input": [{"role": "user", "content": content}],
"text": {
"format": {
"type": "json_schema",
"name": "eapil_llmapi_structured_smoke",
"strict": True,
"schema": SCHEMA,
}
},
"truncation": "disabled",
"max_output_tokens": args.max_output_tokens,
}
if args.reasoning_effort not in {"auto", "default", "omit"}:
payload["reasoning"] = {"effort": args.reasoning_effort}
return payload
def chat_payload(args: argparse.Namespace) -> dict[str, Any]:
content: list[dict[str, Any]] = [
{
"type": "text",
"text": (
"只输出一个 JSON object,不要 markdown。"
"判断输入是否可用于结构化输出 smoke test。"
),
}
]
for image in args.image:
content.append(
{
"type": "image_url",
"image_url": {"url": data_url(Path(image)), "detail": args.image_detail},
}
)
payload: dict[str, Any] = {
"model": args.model,
"messages": [{"role": "user", "content": content}],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "eapil_llmapi_structured_smoke",
"strict": True,
"schema": SCHEMA,
},
},
"temperature": 0,
"max_tokens": args.max_output_tokens,
}
if args.reasoning_effort not in {"auto", "default", "omit"}:
payload["reasoning_effort"] = args.reasoning_effort
return payload
def extract_responses_text(response: dict[str, Any]) -> str:
if isinstance(response.get("output_text"), str):
return str(response["output_text"])
texts: list[str] = []
for item in response.get("output") or []:
if not isinstance(item, dict):
continue
for content in item.get("content") or []:
if isinstance(content, dict) and isinstance(content.get("text"), str):
texts.append(str(content["text"]))
return "\n".join(texts) if texts else json.dumps(response, ensure_ascii=False)
def extract_chat_text(response: dict[str, Any]) -> str:
choices = response.get("choices")
if isinstance(choices, list) and choices:
message = choices[0].get("message") if isinstance(choices[0], dict) else None
content = message.get("content") if isinstance(message, dict) else None
if isinstance(content, str):
return content
return json.dumps(response, ensure_ascii=False)
def post_json(url: str, payload: dict[str, Any], api_key: str, timeout_s: float) -> dict[str, Any]:
body = json.dumps(payload).encode("utf-8")
req = request.Request(
url,
data=body,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
method="POST",
)
with request.urlopen(req, timeout=timeout_s) as response: # noqa: S310
return json.loads(response.read().decode("utf-8"))
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--base-url", default="https://api-n-cd.playones.com/v1")
parser.add_argument("--model", default="gpt-5.5")
parser.add_argument("--endpoint", choices=["responses", "chat"], default="responses")
parser.add_argument("--reasoning-effort", default="none")
parser.add_argument("--max-output-tokens", type=int, default=300)
parser.add_argument("--image", action="append", default=[])
parser.add_argument("--image-detail", default="low")
parser.add_argument("--api-key-env", default="OPENAI_API_KEY")
parser.add_argument("--timeout-s", type=float, default=240.0)
parser.add_argument("--dry-run", action="store_true")
return parser.parse_args()
def main() -> int:
args = parse_args()
payload = responses_payload(args) if args.endpoint == "responses" else chat_payload(args)
path = "responses" if args.endpoint == "responses" else "chat/completions"
url = endpoint_url(args.base_url, path)
if args.dry_run:
print(json.dumps({"url": url, "payload": payload}, ensure_ascii=False, indent=2))
return 0
api_key = os.getenv(args.api_key_env)
if not api_key:
print(f"Missing API key env: {args.api_key_env}", file=sys.stderr)
return 2
started = time.perf_counter()
response = post_json(url, payload, api_key, args.timeout_s)
elapsed = time.perf_counter() - started
raw_text = (
extract_responses_text(response)
if args.endpoint == "responses"
else extract_chat_text(response)
)
try:
parsed = json.loads(raw_text)
parse_ok = isinstance(parsed, dict)
except json.JSONDecodeError:
parse_ok = False
output_types = [
item.get("type")
for item in response.get("output", [])
if isinstance(item, dict)
]
print(
json.dumps(
{
"endpoint": args.endpoint,
"elapsed_s": round(elapsed, 3),
"usage": response.get("usage"),
"raw_text_chars": len(raw_text),
"raw_text_preview": raw_text[:240],
"parse_ok": parse_ok,
"has_encrypted_reasoning": "encrypted_content" in json.dumps(
response,
ensure_ascii=False,
),
"output_item_types": output_types,
},
ensure_ascii=False,
indent=2,
)
)
return 0 if parse_ok else 1
if __name__ == "__main__":
raise SystemExit(main())