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:
@@ -291,6 +291,18 @@
|
|||||||
"authentication": "ON_INSTALL"
|
"authentication": "ON_INSTALL"
|
||||||
},
|
},
|
||||||
"category": "MCP"
|
"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"
|
"authentication": "ON_INSTALL"
|
||||||
},
|
},
|
||||||
"category": "MCP"
|
"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 |
+175
@@ -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.
|
||||||
+4
@@ -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."
|
||||||
+221
@@ -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())
|
||||||
Reference in New Issue
Block a user