From 44448fe75f6fadf9f964e7affb5eabbb1f7f4cc1 Mon Sep 17 00:00:00 2001 From: KeyInfo Bot Date: Thu, 18 Jun 2026 09:57:10 +0800 Subject: [PATCH] =?UTF-8?q?Publish=20=E6=98=93=E7=9E=B3LLM=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=8C=96=E8=B0=83=E7=94=A8=20skill=20to=20marketplace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .agents/plugins/marketplace.json | 12 + .../codex/.agents/plugins/marketplace.json | 12 + .../.codex-plugin/plugin.json | 39 ++++ .../assets/market-icon.svg | 1 + .../skills/eapil-llmapi-structured/SKILL.md | 175 ++++++++++++++ .../agents/openai.yaml | 4 + .../scripts/smoke_structured_output.py | 221 ++++++++++++++++++ 7 files changed, 464 insertions(+) create mode 100644 plugins/codex/plugins/eapil-llmapi-structured/.codex-plugin/plugin.json create mode 100644 plugins/codex/plugins/eapil-llmapi-structured/assets/market-icon.svg create mode 100644 plugins/codex/plugins/eapil-llmapi-structured/skills/eapil-llmapi-structured/SKILL.md create mode 100644 plugins/codex/plugins/eapil-llmapi-structured/skills/eapil-llmapi-structured/agents/openai.yaml create mode 100644 plugins/codex/plugins/eapil-llmapi-structured/skills/eapil-llmapi-structured/scripts/smoke_structured_output.py diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 03158adec..e1a1b718f 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -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": "开发工具" } ] } diff --git a/plugins/codex/.agents/plugins/marketplace.json b/plugins/codex/.agents/plugins/marketplace.json index f83383e52..7098920a5 100644 --- a/plugins/codex/.agents/plugins/marketplace.json +++ b/plugins/codex/.agents/plugins/marketplace.json @@ -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": "开发工具" } ] } diff --git a/plugins/codex/plugins/eapil-llmapi-structured/.codex-plugin/plugin.json b/plugins/codex/plugins/eapil-llmapi-structured/.codex-plugin/plugin.json new file mode 100644 index 000000000..1adce2af8 --- /dev/null +++ b/plugins/codex/plugins/eapil-llmapi-structured/.codex-plugin/plugin.json @@ -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" + } +} diff --git a/plugins/codex/plugins/eapil-llmapi-structured/assets/market-icon.svg b/plugins/codex/plugins/eapil-llmapi-structured/assets/market-icon.svg new file mode 100644 index 000000000..ac846a3e0 --- /dev/null +++ b/plugins/codex/plugins/eapil-llmapi-structured/assets/market-icon.svg @@ -0,0 +1 @@ +易瞳LLM结构化调用 diff --git a/plugins/codex/plugins/eapil-llmapi-structured/skills/eapil-llmapi-structured/SKILL.md b/plugins/codex/plugins/eapil-llmapi-structured/skills/eapil-llmapi-structured/SKILL.md new file mode 100644 index 000000000..8b88613e3 --- /dev/null +++ b/plugins/codex/plugins/eapil-llmapi-structured/skills/eapil-llmapi-structured/SKILL.md @@ -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. diff --git a/plugins/codex/plugins/eapil-llmapi-structured/skills/eapil-llmapi-structured/agents/openai.yaml b/plugins/codex/plugins/eapil-llmapi-structured/skills/eapil-llmapi-structured/agents/openai.yaml new file mode 100644 index 000000000..6c2e27bef --- /dev/null +++ b/plugins/codex/plugins/eapil-llmapi-structured/skills/eapil-llmapi-structured/agents/openai.yaml @@ -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." diff --git a/plugins/codex/plugins/eapil-llmapi-structured/skills/eapil-llmapi-structured/scripts/smoke_structured_output.py b/plugins/codex/plugins/eapil-llmapi-structured/skills/eapil-llmapi-structured/scripts/smoke_structured_output.py new file mode 100644 index 000000000..278d7e83d --- /dev/null +++ b/plugins/codex/plugins/eapil-llmapi-structured/skills/eapil-llmapi-structured/scripts/smoke_structured_output.py @@ -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())