From 965c5aea05f1b7776739b981690fff9d249bd3dc Mon Sep 17 00:00:00 2001 From: "J.Jason" <130959319+JJasonSun@users.noreply.github.com> Date: Sat, 22 Aug 2026 18:04:33 +0800 Subject: [PATCH] Refactor ECNU API skill around safe workflows and reproducible checks - make SKILL.md task-oriented with focused reference routing - remove the undocumented LangChain dimensions request - separate documented contracts from dated live deviations - add safe fallbacks for model discovery and Anthropic long-context suffixes - add sanitized smoke tests, offline validation, unit tests, and CI - remove machine-specific deployment instructions --- .github/workflows/validate.yml | 19 + .gitignore | 7 +- AGENTS.md | 106 ++++-- README.md | 156 +++++--- SKILL.md | 312 +++++++++------- references/api_reference.md | 659 ++++++++++----------------------- references/examples.md | 390 +++++-------------- references/known_deviations.md | 52 +++ references/models.md | 420 ++++++--------------- references/workflows.md | 202 ++++++++++ scripts/smoke_test.py | 452 ++++++++++++++++++++++ scripts/validate_skill.py | 160 ++++++++ tests/test_smoke_test.py | 71 ++++ 13 files changed, 1708 insertions(+), 1298 deletions(-) create mode 100644 .github/workflows/validate.yml create mode 100644 references/known_deviations.md create mode 100644 references/workflows.md create mode 100755 scripts/smoke_test.py create mode 100755 scripts/validate_skill.py create mode 100644 tests/test_smoke_test.py diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..0139ca3 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,19 @@ +name: Validate skill + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Run repository validation + run: python scripts/validate_skill.py + - name: Run unit tests + run: python -m unittest discover -s tests -v diff --git a/.gitignore b/.gitignore index 2e26d63..0bced4e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ +.DS_Store +.env +.env.* +!.env.example .venv/ -.pytest_cache/ __pycache__/ *.py[cod] +smoke-results*.json +smoke-results/ diff --git a/AGENTS.md b/AGENTS.md index 4b19ff3..4c1d42d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,43 +1,85 @@ -# ecnu-api skill repo +# ecnu-api repository guide -Agent Skills package for the ECNU LLM Open Platform API. The repo content IS -the skill; there is no runnable application code. +This repository is an Agent Skills package for the ECNU LLM Open Platform API. +The repository content is the skill; there is no production application. -## Deploy +## Scope -Edit files in this repo, then copy the changed files to -`C:\Users\Jason\.agents\skills\ecnu-api\` to take effect. That directory is a -full mirror of this repo; verify file hashes match after every sync. +- `SKILL.md` — concise activation and execution instructions +- `references/api_reference.md` — documented endpoint contracts +- `references/models.md` — models, aliases, credits, quotas, and deployment +- `references/examples.md` — minimal, safe examples +- `references/workflows.md` — implementation, debugging, retry, and test flows +- `references/known_deviations.md` — dated live observations only +- `scripts/smoke_test.py` — opt-in live structural checks +- `scripts/validate_skill.py` — deterministic repository validation +- `tests/` — offline tests for helper behavior -## Layout +Do not write outside the repository unless the user explicitly asks to install +or synchronize the skill into a client-specific directory. -- `SKILL.md` — skill entry: protocol roots, endpoint map, critical contracts -- `references/api_reference.md` — exact request fields, limits, response shapes -- `references/models.md` — models, aliases, credits, quotas, Recent Changes log -- `references/examples.md` — Python and HTTP examples -- `README.md` — human-facing overview for GitHub +## Source precedence -## Conventions +When ECNU documentation pages disagree: -- Official docs at developer.ecnu.edu.cn are the authority. Never invent - undocumented limits; write "not documented" instead. -- `references/models.md` keeps a `Recent Changes` section; new entries go on - top, newest first. -- Content is English; keep official Chinese terms (voice names, UI labels) - as-is. -- Never commit real API keys. +1. Use the current model page for model identity, context figures, aliases, and + capability labels. +2. Use the endpoint page for JSON fields, types, and endpoint-specific limits. +3. Use the quota page for current prices and quota periods. +4. Use release notes to establish when a change occurred. +5. Use `GET /models` for runtime visibility, not as the sole source of + capability truth. +6. Keep live probes in `known_deviations.md`; never let a single observation + silently override a documented contract. -## Current state (2026-08-22) +## Change workflow -- Synced with official docs through v3.2.1 (2026-08-10) and the doc - restructure of 2026-08-09 (security/tos pages published; vision page merged - into the completions multimodal section). -- Responses-API `reasoning.effort` is documented from release notes; the - responses.html page itself still lags. -- Live-verified against the service on 2026-08-21; deviations are recorded in - `references/api_reference.md` under Live Verification Notes. +1. Work on a branch. +2. Identify the exact official pages affected by the change. +3. Update only the relevant focused reference. +4. If live testing is needed, use `ECNU_API_KEY` from the environment. +5. Never paste or persist a real key in a file, command example, report, issue, + commit, or pull request. +6. Run: -## Verifying changes +```bash +python scripts/validate_skill.py +python -m unittest discover -s tests -v +uvx --from skills-ref agentskills validate . +``` -Fetch each page listed in Official Sources, diff against the reference files, -patch stale statements, then sync to the `.agents` mirror and verify hashes. +7. Review the diff for secrets, machine-specific paths, duplicated guidance, + undocumented request fields, and accidental billable calls. +8. Summarize whether each changed claim is documented, observed, or unverified. + +## Live verification rules + +- The default smoke test performs model-list checks only. +- Chat, embedding, and Anthropic probes require `--low-cost` or `--anthropic`. +- Do not add image generation to an automatic or CI smoke test. +- Do not blindly retry image, TTS, or any other billable request after an + ambiguous network failure. +- Record SDK or Python version, account type, date, endpoint, status, content + type, and structural result. +- Sanitize reports before sharing or committing them. +- Update the date in `known_deviations.md` only when the behavior was actually + reproduced. + +## Content conventions + +- Write documentation in English; retain official Chinese UI labels where + needed. +- Prefer imperative, stepwise instructions over broad prose. +- Keep `SKILL.md` below the Agent Skills recommended size and route detail to + focused references. +- Never describe `skills-ref` validation as an API correctness test. +- Never claim undocumented limits. +- Never describe an output dimension as a request parameter unless ECNU + documents it. +- Use environment variables in every credential example. + +## Current state + +The reference content is aligned with the ECNU documentation and repository +observations available on 2026-08-22. The live deviations remain dated +2026-08-21 until a new authenticated run reproduces or supersedes them. diff --git a/README.md b/README.md index 0788df3..a35867e 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,134 @@ # ECNU API Agent Skill -Unofficial community [Agent Skill](https://agentskills.io/) for working with the -ECNU / ChatECNU LLM Open Platform API. - -This skill helps compatible AI agents answer questions and write integrations -for: - -- OpenAI-compatible chat completions -- OpenAI-compatible Responses API -- Vision / multimodal chat -- Embeddings and rerank -- Image generation -- Text-to-speech -- Structured output +Unofficial community [Agent Skill](https://agentskills.io/) for implementing, +reviewing, testing, and troubleshooting integrations with the ECNU / ChatECNU +LLM Open Platform API. + +The skill covers: + +- OpenAI-compatible Chat Completions and Responses APIs +- vision and multimodal messages +- embeddings and rerank +- image generation and text-to-speech +- structured output - Anthropic-compatible API usage -- Models, authentication, quotas, and error handling +- model selection, authentication, quotas, errors, and known service deviations ## Install -Install with the open Skills CLI: - ```bash npx skills add JJasonSun/ecnu-api ``` -See the skill on [skills.sh](https://skills.sh/jjasonsun/ecnu-api/ecnu-api). - -Alternatively, clone or copy this repository into the skills directory used by -your Agent Skills-compatible client. Keep the installed directory name as -`ecnu-api`, because the Agent Skills specification requires it to match the -`name` in `SKILL.md`. +Or copy this repository into the skills directory used by an Agent +Skills-compatible client. Keep the installed directory name as `ecnu-api` so it +matches the `name` in `SKILL.md`. -The exact skills directory depends on the client. For example: +Example invocation: ```text -/ecnu-api/SKILL.md +Use $ecnu-api to review this ECNU API integration. ``` -Once installed, ask the agent to work with the ECNU API. Clients that support -explicit skill invocation may also accept prompts such as: +## Repository layout ```text -Use $ecnu-api to help me integrate with the ECNU LLM Open Platform API. +ecnu-api/ +├── SKILL.md +├── AGENTS.md +├── references/ +│ ├── api_reference.md +│ ├── models.md +│ ├── examples.md +│ ├── workflows.md +│ └── known_deviations.md +├── scripts/ +│ ├── smoke_test.py +│ └── validate_skill.py +├── tests/ +│ └── test_smoke_test.py +└── .github/workflows/validate.yml +``` + +`SKILL.md` contains the core workflow and tells an agent when to load each +focused reference. Live observations are isolated from documented contracts in +`references/known_deviations.md`. + +## Configure a key safely + +Store the key in an environment variable. Do not put it in source files, shell +scripts, screenshots, committed reports, or chat prompts. + +PowerShell: + +```powershell +$env:ECNU_API_KEY = "your-api-key" ``` -## Files +macOS or Linux: + +```bash +export ECNU_API_KEY="your-api-key" +``` -- `SKILL.md`: skill trigger metadata and quick navigation. -- `AGENTS.md`: repo maintenance guide for AI agents (deploy flow, - conventions, verification workflow). -- `references/api_reference.md`: endpoint summaries and request/response notes. -- `references/models.md`: models, aliases, credits, quotas, and errors. -- `references/examples.md`: short Python SDK and direct HTTP examples. +A key pasted into a chat or public location should be revoked or rotated after +testing. -## Validate +## Reproducible smoke tests -Run the official -[`skills-ref`](https://github.com/agentskills/agentskills/tree/main/skills-ref) -reference validator with `uv`: +The default profile performs model-list checks and does not send chat, +embedding, Anthropic, image, or TTS POST requests: ```bash -uvx --from skills-ref agentskills validate /path/to/ecnu-api +python scripts/smoke_test.py ``` -On Windows PowerShell, force UTF-8 when the system locale is not UTF-8: +Low-cost POST probes are explicit: -```powershell -$env:PYTHONUTF8 = "1" -uvx --from skills-ref agentskills validate C:\path\to\ecnu-api +```bash +python scripts/smoke_test.py --low-cost --anthropic \ + --account-type personal-token \ + --output smoke-results.json ``` -## Official Documentation +The report contains statuses and structural summaries. It does not print the +API key or successful model content. Image generation is intentionally absent +from the automated smoke test because it is comparatively expensive and a +retry after an ambiguous failure could duplicate charges. -API details can change. Treat this skill as a working summary and verify -production-critical details against the official ECNU developer docs: +## Validate the skill + +Run deterministic repository checks and unit tests: + +```bash +python scripts/validate_skill.py +python -m unittest discover -s tests -v +``` + +Run the Agent Skills reference validator separately: + +```bash +uvx --from skills-ref agentskills validate . +``` + +The reference validator checks format and naming conventions; it does not +verify that ECNU endpoints are currently available or that every documented +contract matches live behavior. + +## Maintenance principles + +- Official ECNU documentation is the authority for documented contracts. +- Runtime observations must include a date and must remain labeled as + observations. +- Do not infer unsupported OpenAI or Anthropic fields. +- Keep examples minimal and secrets environment-based. +- Do not add local absolute paths or machine-specific deployment instructions. +- Run repository validation before opening a pull request. + +## Official documentation + +API details can change. Verify production-critical behavior against the current +ECNU developer documentation: - https://developer.ecnu.edu.cn/vitepress/llm/model.html - https://developer.ecnu.edu.cn/vitepress/llm/thinking.html @@ -84,17 +139,16 @@ production-critical details against the official ECNU developer docs: - https://developer.ecnu.edu.cn/vitepress/llm/api/models.html - https://developer.ecnu.edu.cn/vitepress/llm/api/completions.html - https://developer.ecnu.edu.cn/vitepress/llm/api/responses.html -- https://developer.ecnu.edu.cn/vitepress/llm/api/vision.html -- https://developer.ecnu.edu.cn/vitepress/llm/api/imagegenerate.html - https://developer.ecnu.edu.cn/vitepress/llm/api/embedding.html - https://developer.ecnu.edu.cn/vitepress/llm/api/rerank.html +- https://developer.ecnu.edu.cn/vitepress/llm/api/imagegenerate.html - https://developer.ecnu.edu.cn/vitepress/llm/api/audio.html - https://developer.ecnu.edu.cn/vitepress/llm/api/anthropic.html - https://developer.ecnu.edu.cn/vitepress/llm/api/structuredoutput.html -- https://developer.ecnu.edu.cn/vitepress/llm/api/embediframe.html +- https://developer.ecnu.edu.cn/vitepress/llm/tos.html ## Disclaimer This is an unofficial community skill. It is not endorsed by or affiliated with -East China Normal University. Do not commit API keys, personal tokens, internal -whitelist details, or screenshots containing credentials. +East China Normal University. Never commit API keys, personal tokens, internal +allowlist details, private prompts, or unsanitized live-test output. diff --git a/SKILL.md b/SKILL.md index eaed423..48dfd06 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,160 +1,190 @@ --- name: ecnu-api description: > - Integrate with the ECNU (East China Normal University) LLM Open Platform. - Covers OpenAI-compatible Chat Completions and Responses APIs, multimodal + Implement, review, test, or troubleshoot integrations with the ECNU + (East China Normal University) LLM Open Platform at chat.ecnu.edu.cn. + Use for OpenAI-compatible Chat Completions and Responses APIs, multimodal input, embeddings, rerank, image generation, text-to-speech, structured - output, model discovery, and the separate Anthropic-compatible API. Use when - an AI agent needs to call or troubleshoot chat.ecnu.edu.cn APIs, select ECNU - models, validate request types and limits, configure an OpenAI or Anthropic - client, or explain authentication, credits, quotas, errors, and compatibility - aliases. Triggers include "ECNU API", "ChatECNU", "华东师范大学 API", - "ECNU 大模型", "ecnu-max", "ecnu-plus", "ecnu-embedding-small", - "ecnu-rerank", "ecnu-image", and "ecnu-tts". + output, model discovery, Anthropic-compatible clients, authentication, + credits, quotas, and API errors. Do not use for general ECNU information + or unrelated DeepSeek and Qwen questions. --- # ECNU LLM Open Platform API -Use the current ECNU developer documentation as the authority. Treat request -types, units, and URL roots as separate contracts; do not infer unsupported -OpenAI parameters merely because an endpoint is OpenAI-compatible. +Use this skill to turn ECNU API documentation into a safe, verifiable +integration. The current official ECNU developer documentation is the +authority for documented contracts. Keep documented facts, live observations, +and application policy separate. -## Choose the Correct Protocol Root +## Core rules -| Protocol | Base or full URL | Use for | -|---|---|---| -| OpenAI-compatible | `https://chat.ecnu.edu.cn/open/api/v1` | Chat Completions, Responses, embeddings, images, TTS, models | -| Anthropic-compatible | `https://chat.ecnu.edu.cn/open/api/anthropic` | Anthropic SDK and `/v1/messages` | -| Embed iFrame (experimental) | `https://chat.ecnu.edu.cn/open/api/embed/app` | One-time embedded ChatECNU URL | +1. Select the protocol root before constructing a request. +2. Use only fields documented by ECNU or explicitly verified against the + current service. +3. Do not assume that every OpenAI or Anthropic feature is implemented merely + because an endpoint is compatible with that protocol. +4. Keep API keys in environment variables. Never put a real key in source, + command history, examples, logs, screenshots, or committed test output. +5. Treat live observations as point-in-time evidence, not permanent contracts. +6. Avoid parallel calls. Run batches sequentially unless ECNU documents a safe + concurrency policy. -Never append the Anthropic path to the OpenAI base. The full Anthropic messages -URL is `https://chat.ecnu.edu.cn/open/api/anthropic/v1/messages`. +## Workflow -Authenticate API calls with: +### 1. Classify the request -```http -Authorization: Bearer -Content-Type: application/json +Determine whether the user wants: + +- an explanation; +- implementation or code review; +- troubleshooting; +- a live capability check; +- a cost or quota calculation. + +Do not execute a real request when the user only asks for documentation or +sample code. + +### 2. Load only the relevant reference + +- Read [references/api_reference.md](references/api_reference.md) for endpoint + roots, request fields, limits, and response shapes. +- Read [references/models.md](references/models.md) for model selection, + aliases, thinking modes, credits, quotas, and deployment notes. +- Read [references/examples.md](references/examples.md) for minimal Python and + HTTP examples. +- Read [references/workflows.md](references/workflows.md) for implementation, + review, troubleshooting, retry, privacy, and live-verification procedures. +- Read [references/known_deviations.md](references/known_deviations.md) when + diagnosing behavior that conflicts with the official documentation. + +Do not load every reference for a narrow task. + +### 3. Select the protocol root + +| Protocol | Base or full URL | +|---|---| +| OpenAI-compatible | `https://chat.ecnu.edu.cn/open/api/v1` | +| Anthropic-compatible | `https://chat.ecnu.edu.cn/open/api/anthropic` | +| Embed iFrame | `https://chat.ecnu.edu.cn/open/api/embed/app` | + +The Anthropic messages URL is: + +```text +https://chat.ecnu.edu.cn/open/api/anthropic/v1/messages ``` -Obtain a key in ChatECNU under the avatar menu, "我的令牌". Never place a real -key in source, examples, logs, screenshots, or error reports. Tokens are -personal, default to a 90-day validity, and must be renewed before expiry. - -## Endpoint Map - -Paths below are relative to the OpenAI-compatible base unless a full URL is -shown. - -| Capability | Method and path | Model | -|---|---|---| -| Chat Completions | `POST /chat/completions` | `ecnu-max`, `ecnu-plus` | -| Responses | `POST /responses` | `ecnu-max`, `ecnu-plus` | -| Vision | `POST /chat/completions` | Prefer `ecnu-plus`; `ecnu-vl` is a legacy alias | -| Embeddings | `POST /embeddings` | `ecnu-embedding-small` | -| Rerank | `POST /rerank` | `ecnu-rerank` | -| Image generation | `POST /images/generations` | `ecnu-image` | -| Text-to-speech | `POST /audio/speech` | `ecnu-tts` | -| Model list | `GET /models` | N/A | -| Structured output | `POST /chat/completions` | `ecnu-plus` and alias `ecnu-turbo` | -| Anthropic messages | `POST https://chat.ecnu.edu.cn/open/api/anthropic/v1/messages` | `ecnu-max`, `ecnu-plus`, mapped aliases | -| Embed iFrame | `POST https://chat.ecnu.edu.cn/open/api/embed/app` | N/A | - -## Current Primary Models - -| Model | Underlying model | Published context | Thinking | Tools | Vision | -|---|---|---|---|---|---| -| `ecnu-max` | DeepSeek-V4-Flash-0731 | 1M | Supported, default off | Yes | No | -| `ecnu-plus` | Qwen3.6-27B | 256K | Supported, default off | Yes | Yes | - -The model page does not label the context figures as tokens or characters. Do -not add a unit. The Anthropic page separately describes `ecnu-max[1m]` as 1M -characters for Anthropic tools. - -Prefer the model page over older endpoint examples when model names conflict. -The former vision page now redirects to the Chat Completions multimodal -section; use `ecnu-plus` for new image-understanding integrations and retain -`ecnu-vl` only for compatibility. - -## Critical Request Contracts - -### Embeddings - -- Send `input` as one string or an array of strings: `string | string[]`. -- Do not send integer token arrays. ECNU uses a non-OpenAI tokenizer and the - official docs explicitly warn that pre-tokenized OpenAI token IDs are not - supported. -- The published input limit is 8192 characters. The docs do not say whether - this applies to each array element or the whole array, and they publish no - maximum batch size. State that ambiguity instead of inventing a limit. -- Output vectors contain 1024 floats. The direct API documents only `model` and - `input`; do not present arbitrary dimensions as supported. -- For LangChain `OpenAIEmbeddings`, set `dimensions=1024` and - `check_embedding_ctx_length=False`. - -### Rerank - -- Send `documents` as a string array and `query` as a string. +Never append the Anthropic path to the OpenAI-compatible `/v1` base. + +### 4. Select a model + +| Task | Preferred model | +|---|---| +| General text, tools, lower latency | `ecnu-plus` | +| Complex text or code | `ecnu-max` | +| Image understanding | `ecnu-plus` | +| Embeddings | `ecnu-embedding-small` | +| Rerank | `ecnu-rerank` | +| Image generation | `ecnu-image` | +| Text-to-speech | `ecnu-tts` | + +Use `ecnu-max` and `ecnu-plus` for new dialog integrations. Treat historical +names as compatibility aliases. + +### 5. Validate the request contract + +#### Embeddings + +- `input` must be one string or an array of strings. +- Do not send OpenAI token-ID arrays. +- ECNU documents a 1024-float output vector. +- The direct ECNU request documents `model` and `input`; do not add an + unsupported dimension-selection request field. +- With LangChain `OpenAIEmbeddings`, set + `check_embedding_ctx_length=False` so raw strings are sent. Verify the + returned vector length after the request. + +#### Rerank + +- `documents` must be a string array. +- `query` must be a string. - Each document is limited to 8192 characters. -- `top_n` defaults to 5. The docs publish no maximum, no document-count limit, - and no query-length limit. Do not fabricate them. -- `return_documents` controls whether document text is returned. - -### Vision - -- Use structured message content with `text` and `image_url` parts. -- `image_url.url` may be a public URL or a base64 data URL. -- The API page publishes no image-count or image-size limit. A ChatECNU UI - release note about five uploaded images is not an API limit. - -### Image and Audio - -- Image prompt: at most 1024 characters. Prompts over 500 characters may be - compressed. Supported sizes are documented in the API reference. -- Image URLs expire after 24 hours; transfer them immediately. -- TTS input: at most 4096 characters. Speed range: 0.25 through 4.0. - -## Operational Rules - -- Avoid parallel API calls. Wait for one response before starting the next to - reduce service-protection failures. -- All capabilities share the credits quota. Dialog usage distinguishes cached - and uncached input; cached input currently costs one fifth of uncached input. -- Enable dialog thinking with `{"thinking": {"type": "enabled"}}`. With the - OpenAI Python SDK, pass this ECNU extension through `extra_body`. -- `ecnu-max` supports `reasoning_effort` (`low` / `high` / `max`) to control - thinking intensity when thinking is enabled. `ecnu-plus` ignores this - parameter. The Anthropic-compatible API uses `output_config.effort` and the - Responses API uses `reasoning.effort`, both with a different set of levels - mapped to `ecnu-max` tiers. -- When thinking is enabled, `temperature` and `top_p` may not take effect or - may be restricted; prefer defaults. -- Outside thinking mode, the model page advises tuning `temperature`, `top_p`, - and other sampling parameters per the underlying models' official - documentation; it publishes no platform-specific defaults. -- Native `search_mode` web search was removed. Use tool calling or an external - search implementation. -- Treat `422` as a request-shape/type failure and inspect `detail`; treat `429` - as quota, rate, or short-term service protection. -- Check current availability at `https://chat.ecnu.edu.cn/status`. - -## Read the Relevant Reference - -- Read [references/api_reference.md](references/api_reference.md) for exact - request fields, limits, response shapes, protocol roots, documented - ambiguities, and the Live Verification Notes on observed docs-vs-service - deviations. -- Read [references/models.md](references/models.md) for model aliases, - deployment notes, current cached/uncached credit formulas, quotas, and errors. -- Read [references/examples.md](references/examples.md) for minimal Python and - HTTP examples, including scalar and array embeddings, Responses API, - Anthropic 1M context, sequential batching, and error handling. +- `top_n` defaults to 5. ECNU publishes no maximum document count, maximum + `top_n`, or query-length limit. + +#### Vision + +- Use Chat Completions with structured `text` and `image_url` content parts. +- Use `ecnu-plus`. +- A public URL or base64 data URL may be used. +- Do not convert a ChatECNU web-UI upload limit into an API limit. + +#### Image and audio + +- Image prompts are limited to 1024 characters; prompts over 500 characters + may be compressed. +- Image URLs expire after 24 hours. +- TTS input is limited to 4096 characters. +- TTS speed is 0.25 through 4.0. + +### 6. Protect secrets, data, and credits + +Before a real request: + +- use an environment variable such as `ECNU_API_KEY`; +- remove secrets and unnecessary personal or confidential data; +- confirm the user intended to send the supplied content to ECNU; +- state when image generation, TTS, or other calls may consume credits; +- never blindly retry a billable request after an ambiguous timeout. + +If a key has already been pasted into a chat or public location, recommend +revoking or rotating it after testing. + +### 7. Execute and verify + +For reproducible checks, run: + +```bash +python scripts/smoke_test.py +``` + +This default profile performs model-list checks only. Low-cost POST probes are +opt-in: + +```bash +python scripts/smoke_test.py --low-cost --anthropic +``` + +The script reads `ECNU_API_KEY`, redacts key-shaped strings, and emits a +structural JSON report rather than model output. + +### 8. Report provenance + +Label important conclusions as one of: + +- **Documented** — supported by the current official ECNU documentation. +- **Observed** — reproduced against the live service at a stated date. +- **Unverified** — inferred, historical, or not reproducible in the current + environment. + +Do not silently promote an observed deviation into a documented guarantee. + +## High-value gotchas -When a production decision depends on a limit the reference marks as -undocumented, verify against the official page or a controlled authenticated -request. Do not turn an observation into a permanent platform guarantee. +- `GET /models` is runtime discovery, not a reliable authentication test. +- A model appearing in `/models` does not prove that a capability is usable. +- Use `ecnu-max[1m]` only when an Anthropic tool requires the suffix to + advertise long context. Fall back to plain `ecnu-max` if the suffix returns + an authentication or metadata error. +- TTS errors may not match the documented JSON shape; preserve the HTTP status, + content type, and a bounded redacted body sample. +- Do not depend on optional PCM metadata headers without checking them at + runtime. +- `422` means request validation failed; inspect `detail`. +- `429` may represent quota exhaustion, rate control, or short-term service + protection. Stop parallel retries and inspect credits first. -## Official Documentation +## Official documentation - Models: https://developer.ecnu.edu.cn/vitepress/llm/model.html - Thinking: https://developer.ecnu.edu.cn/vitepress/llm/thinking.html @@ -163,6 +193,6 @@ request. Do not turn an observation into a permanent platform guarantee. - Quotas: https://developer.ecnu.edu.cn/vitepress/llm/limit.html - Errors: https://developer.ecnu.edu.cn/vitepress/llm/error.html - Release notes: https://developer.ecnu.edu.cn/vitepress/llm/release.html -- Local deployment and data security: https://developer.ecnu.edu.cn/vitepress/llm/security.html -- Developer agreement (token rules): https://developer.ecnu.edu.cn/vitepress/llm/tos.html +- Data security: https://developer.ecnu.edu.cn/vitepress/llm/security.html +- Developer agreement: https://developer.ecnu.edu.cn/vitepress/llm/tos.html - Service status: https://chat.ecnu.edu.cn/status diff --git a/references/api_reference.md b/references/api_reference.md index 2bde656..ed38d7e 100644 --- a/references/api_reference.md +++ b/references/api_reference.md @@ -1,28 +1,9 @@ # ECNU API Reference -This reference separates documented facts from compatibility assumptions. A -field listed by the OpenAI or Anthropic API is not automatically supported by -ECNU; use only fields documented here or verified with the current service. - -## Table of Contents - -- [Protocol Roots and Authentication](#protocol-roots-and-authentication) -- [Chat Completions](#chat-completions) -- [Responses API](#responses-api) -- [Vision](#vision) -- [Embeddings](#embeddings) -- [Rerank](#rerank) -- [Image Generation](#image-generation) -- [Text-to-Speech](#text-to-speech) -- [Model List](#model-list) -- [Anthropic-Compatible API](#anthropic-compatible-api) -- [Structured Output](#structured-output) -- [Embed iFrame](#embed-iframe) -- [Errors and Undocumented Limits](#errors-and-undocumented-limits) -- [Live Verification Notes](#live-verification-notes) -- [Official Sources](#official-sources) - -## Protocol Roots and Authentication +This file contains documented request contracts. Point-in-time service +differences belong in [known_deviations.md](known_deviations.md), not here. + +## Protocol roots and authentication ### OpenAI-compatible APIs @@ -30,41 +11,47 @@ ECNU; use only fields documented here or verified with the current service. https://chat.ecnu.edu.cn/open/api/v1 ``` -Use this base for `/chat/completions`, `/responses`, `/embeddings`, `/rerank`, -`/images/generations`, `/audio/speech`, and `/models`. +Use this base for Chat Completions, Responses, embeddings, rerank, images, TTS, +and models. ### Anthropic-compatible API ```text Base: https://chat.ecnu.edu.cn/open/api/anthropic -Full messages URL: https://chat.ecnu.edu.cn/open/api/anthropic/v1/messages +Messages: https://chat.ecnu.edu.cn/open/api/anthropic/v1/messages ``` -This is a separate protocol root. Do not append `/anthropic/v1/messages` to the -OpenAI-compatible base. - -### Embed iFrame API +### Embed iFrame ```text https://chat.ecnu.edu.cn/open/api/embed/app ``` -This experimental endpoint is also outside the OpenAI-compatible `/v1` root. - ### Authentication ```http -Authorization: Bearer +Authorization: Bearer Content-Type: application/json ``` -Get the key from ChatECNU under the avatar menu, "我的令牌". A missing or -invalid token returns `401`. Some third-party applications also require an IP -allowlist; a mismatch returns `403`. +Use environment variables. Tokens are personal and the developer agreement +states that their default validity is 90 days. + +## Endpoint map -Per the developer agreement, tokens are personal (do not lend them to others or -expose them in browser or client code), default to a 90-day validity, and must -be renewed before expiry. +| Capability | Method and path | Model | +|---|---|---| +| Chat Completions | `POST /chat/completions` | `ecnu-max`, `ecnu-plus` | +| Responses | `POST /responses` | `ecnu-max`, `ecnu-plus` | +| Vision | `POST /chat/completions` | `ecnu-plus` | +| Embeddings | `POST /embeddings` | `ecnu-embedding-small` | +| Rerank | `POST /rerank` | `ecnu-rerank` | +| Image generation | `POST /images/generations` | `ecnu-image` | +| Text-to-speech | `POST /audio/speech` | `ecnu-tts` | +| Model list | `GET /models` | N/A | +| Structured output | `POST /chat/completions` | `ecnu-plus`, `ecnu-turbo` | +| Anthropic messages | full URL above | dialog models and mappings | +| Embed iFrame | full URL above | N/A | ## Chat Completions @@ -72,136 +59,80 @@ be renewed before expiry. POST https://chat.ecnu.edu.cn/open/api/v1/chat/completions ``` -### Documented request fields - -| Field | JSON type | Required | Contract | -|---|---|---|---| -| `model` | string | Yes | Prefer `ecnu-max` or `ecnu-plus` | -| `messages` | array | Yes | Ordered message objects | -| `messages[].role` | string | Yes | `system`, `user`, or `assistant` | -| `messages[].content` | string or array | Yes | String for text; structured parts for vision | -| `stream` | boolean | No | Return Server-Sent Events when true | -| `temperature` | number | No | 0 through 1; model-specific default; may be restricted when thinking is enabled | -| `top_p` | number | No | 0 through 1; model-specific default; may be restricted when thinking is enabled | -| `tools` | array | No | OpenAI-compatible function definitions | -| `tools[].type` | string | With tools | Fixed to `function` | -| `tools[].function.name` | string | With tools | Function name | -| `tools[].function.description` | string | With tools | Function description | -| `tools[].function.parameters` | object | With tools | JSON Schema-like parameters | -| `thinking` | object | No | ECNU extension: `{"type":"enabled"}` or `{"type":"disabled"}` | -| `reasoning_effort` | string | No | ECNU extension: `low`, `high`, or `max`; only `ecnu-max` with thinking enabled; `ecnu-plus` ignores it | -| `response_format` | object | No | Structured output; see below | -| `max_tokens` | integer | For bounded output | Used by ECNU's structured-output examples; publish no universal maximum | - -`search_mode` remains visible in older request tables but native web search was -removed on 2025-03-20. Do not use it for new integrations. - -When using the OpenAI Python SDK, pass `thinking` and `reasoning_effort` through -`extra_body` because they are ECNU extensions rather than standard SDK keywords: - -```python -client.chat.completions.create( - model="ecnu-max", - messages=[{"role": "user", "content": "Analyze this."}], - extra_body={ - "thinking": {"type": "enabled"}, - "reasoning_effort": "high", - }, -) -``` +Documented request fields include: -`reasoning_effort` only takes effect when `thinking` is set to `enabled` and -only applies to `ecnu-max`. When thinking is enabled, `temperature` and `top_p` -may not take effect or may be restricted; prefer defaults. - -In multi-turn conversations under thinking mode, if the assistant called a -tool, its `reasoning_content` must be included in all subsequent turns; some -models return `400` if it is missing. If no tool was called, `reasoning_content` -can be omitted from subsequent context. - -### Response fields - -| Field | Meaning | -|---|---| -| `id` | Completion ID | -| `object` | Object type, normally `chat.completion` | -| `created` | Creation timestamp | -| `choices[].index` | Choice index | -| `choices[].message.role` | Assistant role | -| `choices[].message.content` | Final content | -| `choices[].message.reasoning_content` | Reasoning content when exposed | -| `choices[].message.tool_calls` | Requested function calls | -| `choices[].finish_reason` | Stop reason | -| `usage.prompt_tokens` | Estimated input usage | -| `usage.completion_tokens` | Estimated output usage | -| `usage.total_tokens` | Estimated total usage | - -With `stream: true`, parse SSE `data:` lines and stop at `data: [DONE]`. +| Field | Type | Notes | +|---|---|---| +| `model` | string | Prefer `ecnu-max` or `ecnu-plus` | +| `messages` | array | Ordered messages | +| `messages[].role` | string | `system`, `user`, or `assistant` | +| `messages[].content` | string or array | Array form is used for vision | +| `stream` | boolean | Streams SSE when true | +| `temperature` | number | 0 through 1 | +| `top_p` | number | 0 through 1 | +| `tools` | array | OpenAI-compatible function definitions | +| `thinking` | object | `{"type":"enabled"}` or `{"type":"disabled"}` | +| `reasoning_effort` | string | `low`, `high`, or `max`; `ecnu-max` only | +| `response_format` | object | Structured output | +| `max_tokens` | integer | Use enough room for complete output | + +Pass ECNU-specific fields through `extra_body` when using the OpenAI Python +SDK. + +`reasoning_effort` only applies when thinking is enabled and only to +`ecnu-max`. `ecnu-plus` ignores it. Sampling controls may not take effect or +may be restricted in thinking mode. + +If a tool was called during a thinking-mode conversation, retain the returned +`reasoning_content` in subsequent turns when required by the model. Do not +expose hidden reasoning to end users merely because a response field exists. + +A non-streaming response follows the OpenAI completion-list shape with +`choices[].message`, `finish_reason`, and `usage`. For streaming, parse SSE +`data:` lines and stop at `[DONE]`. + +Native `search_mode` web search was removed. Implement search through tool +calling or an external search service. ## Responses API -ECNU supports the OpenAI Responses wire format for both `ecnu-plus` and -`ecnu-max`. - ```http POST https://chat.ecnu.edu.cn/open/api/v1/responses ``` -The official page currently documents the SDK form, model selection, and Codex -provider configuration, but does not publish a complete ECNU-specific field or -event table. Use the standard OpenAI client shape conservatively: +Both primary dialog models support the Responses wire format. The ECNU page +does not publish a complete ECNU-specific field and event matrix. Start with +text input and verify advanced OpenAI Responses tools or event types before +depending on them. -```python -response = client.responses.create( - model="ecnu-max", - input="Summarize this request.", -) -print(response.output_text) -``` - -Do not assume every OpenAI Responses tool or event type is implemented until it -is documented or verified. Requests use the same credits pool as other dialog -calls. - -### Responses-API thinking effort - -The Responses-compatible API supports `reasoning.effort` to control thinking -intensity for `ecnu-max`. Passing `reasoning.effort: "none"` disables thinking; -when omitted, the server default applies. The proxy applies the same tier -mapping as the Anthropic-compatible API. +For `ecnu-max`, `reasoning.effort` controls thinking intensity. The compatibility +layer maps its levels to ECNU tiers; `none` disables thinking. ## Vision -Vision uses the Chat Completions endpoint. Prefer `ecnu-plus`. The former -dedicated vision page now redirects to the completions page's multimodal -section, which documents `ecnu-plus` multimodal messages; the model page -defines `ecnu-vl` as a compatibility alias for `ecnu-plus`. - -Use an array of content parts: +Use Chat Completions with `ecnu-plus`: ```json { - "role": "user", - "content": [ - {"type": "text", "text": "Describe this image."}, + "model": "ecnu-plus", + "messages": [ { - "type": "image_url", - "image_url": {"url": "data:image/jpeg;base64,"} + "role": "user", + "content": [ + {"type": "text", "text": "Describe this image."}, + { + "type": "image_url", + "image_url": {"url": "data:image/jpeg;base64,"} + } + ] } ] } ``` -Documented content-part types: - -| Type | Required value | -|---|---| -| `text` | `text` string | -| `image_url` | `image_url.url`, as a public URL or base64 data URL | - -The API page does not publish supported MIME types, byte limits, pixel limits, -or a maximum number of images. Do not reuse the ChatECNU web UI's five-upload -release note as an API contract. +`image_url.url` may be a public URL or a base64 data URL. The API page does not +publish a maximum image count, byte size, pixel size, or MIME-type matrix. +Do not reuse a web-UI upload limit as an API contract. ## Embeddings @@ -209,80 +140,22 @@ release note as an API contract. POST https://chat.ecnu.edu.cn/open/api/v1/embeddings ``` -### Request contract - -| Field | JSON type | Required | Contract | +| Field | Type | Required | Contract | |---|---|---|---| -| `model` | string | Yes | Fixed to `ecnu-embedding-small` | -| `input` | string or string[] | Yes | One text or an array of texts | +| `model` | string | Yes | `ecnu-embedding-small` | +| `input` | string or string[] | Yes | Raw text only | -Examples of valid input shapes: +Unsupported forms include integer token IDs and arrays of integer token arrays. +ECNU uses a non-OpenAI tokenizer. -```json -{"model":"ecnu-embedding-small","input":"one text"} -``` +The published input limit is 8192 characters, but the page does not specify +whether an array is checked per item, by combined length, or both. It publishes +no maximum batch item count. -```json -{"model":"ecnu-embedding-small","input":["first text","second text"]} -``` - -Do not send either of these unsupported shapes: - -```json -{"input":[123,456,789]} -``` - -```json -{"input":[[123,456],[789]]} -``` - -Those are OpenAI token-ID forms, not string arrays. ECNU's documentation -explicitly explains that OpenAI-tokenized integer input is incompatible with -the non-OpenAI embedding model. - -### Limits and dimensions - -- The official request table says `input` must not exceed 8192 characters. -- It does not state whether an array is limited per element, by total combined - characters, or both. -- It does not publish a maximum array length or request byte size. -- The output is fixed at 1024 floating-point values. -- The direct request table documents no `dimensions` parameter. Do not request - another size. The official LangChain example sets `dimensions=1024` only to - describe the fixed output size to LangChain. - -For production batching, validate that every item is a string, keep batches -conservative, submit sequentially, and split a batch if the service returns -`422`. Do not claim a guessed batch maximum as an ECNU limit. - -### Response contract - -The response follows the OpenAI list shape: - -| Field | Meaning | -|---|---| -| `object` | `list` | -| `data[].object` | `embedding` | -| `data[].embedding` | 1024-float vector | -| `data[].index` | Position corresponding to the input array | -| `model` | `ecnu-embedding-small` | -| `usage.prompt_tokens` | Estimated input usage | -| `usage.total_tokens` | Estimated total usage | - -For LangChain: - -```python -OpenAIEmbeddings( - base_url="https://chat.ecnu.edu.cn/open/api/v1", - api_key=api_key, - model="ecnu-embedding-small", - dimensions=1024, - check_embedding_ctx_length=False, -) -``` - -`check_embedding_ctx_length=False` prevents LangChain from converting strings -to OpenAI token IDs before sending them. +The output contains 1024 floating-point values per embedding. The direct +request contract does not document a dimension-selection field. With LangChain, +disable automatic token-length conversion and validate output length after the +response. ## Rerank @@ -290,209 +163,88 @@ to OpenAI token IDs before sending them. POST https://chat.ecnu.edu.cn/open/api/v1/rerank ``` -The request is Cohere-compatible, not part of the OpenAI SDK surface. +The request is Cohere-compatible rather than part of the OpenAI SDK surface. -| Field | JSON type | Required | Contract | +| Field | Type | Required | Contract | |---|---|---|---| -| `model` | string | Yes | Fixed to `ecnu-rerank` | -| `documents` | string[] | Yes | Candidate documents; each at most 8192 characters | +| `model` | string | Yes | `ecnu-rerank` | +| `documents` | string[] | Yes | Each document at most 8192 characters | | `query` | string | Yes | Search query | -| `return_documents` | boolean | No | Include document text in results | -| `top_n` | integer | No | Number returned; default 5 | - -The official page publishes no maximum document count, maximum `top_n`, or -query-length limit. A caller should normally keep `top_n <= documents.length`, -but that is client-side logic, not a published ECNU constraint. +| `return_documents` | boolean | No | Include document text | +| `top_n` | integer | No | Defaults to 5 | -Response fields: - -| Field | Meaning | -|---|---| -| `id` | Request ID | -| `results[].index` | Index into the submitted `documents` array | -| `results[].relevance_score` | Relevance score | -| `results[].document` | Document text when returned | +No maximum document count, maximum `top_n`, or query-length limit is published. -## Image Generation +## Image generation ```http POST https://chat.ecnu.edu.cn/open/api/v1/images/generations ``` -| Field | JSON type | Required | Contract | +| Field | Type | Required | Contract | |---|---|---|---| | `model` | string | Yes | `ecnu-image` | -| `prompt` | string | Yes | At most 1024 characters; over 500 may be compressed | -| `size` | string | No | See supported values below; default `512x512` | -| `response_format` | string | No | `url` or `b64_json`; default `url` | +| `prompt` | string | Yes | At most 1024 characters | +| `size` | string | No | Defaults to `512x512` | +| `response_format` | string | No | `url` or `b64_json` | -Supported sizes: +Documented sizes: -`512x512`, `768x768`, `720x1280`, `1280x720`, `1024x1024` +```text +512x512 +768x768 +720x1280 +1280x720 +1024x1024 +``` -`data[].url` is retained for 24 hours only. `data[].b64_json` is returned for -base64 format. `data[].revised_prompt` may contain the service-adjusted prompt. -Generation failures can return `err_message` and a masked or revised prompt. +Prompts over 500 characters may be compressed. URL results are retained for 24 +hours, so transfer them promptly. Treat retries after ambiguous failures as +potential duplicate charges. -## Text-to-Speech +## Text-to-speech ```http POST https://chat.ecnu.edu.cn/open/api/v1/audio/speech ``` -| Field | JSON type | Required | Contract | +| Field | Type | Required | Contract | |---|---|---|---| | `model` | string | Yes | `ecnu-tts` | | `input` | string | Yes | At most 4096 characters | -| `voice` | string | No | Voice ID from the voice list below; default `xiayu` | -| `response_format` | string | No | `mp3`, `opus`, `aac`, `flac`, `wav`, `pcm`; default `mp3` | -| `speed` | number | No | 0.25 through 4.0; default 1.0 | - -The successful response body is binary audio with a format-specific MIME type. -The response includes a `Content-Disposition` header with a suggested filename. -When `response_format` is `pcm`, the response also includes `Content-Rate` -(sample rate), `Content-Channels` (fixed to 1), and `Content-Bits` (fixed to 16) -headers for direct playback. +| `voice` | string | No | Defaults to `xiayu` | +| `response_format` | string | No | `mp3`, `opus`, `aac`, `flac`, `wav`, `pcm` | +| `speed` | number | No | 0.25 through 4.0 | -### TTS voices +The success body is binary audio with a format-specific content type. Do not +parse it as JSON. The official docs and model page may not list voices in the +same place or at the same update time; consult the current TTS page before +validating a voice ID. -`ecnu-tts` supports 16 voice types. Dialect and character voices are trained on -specific corpora; test with short text before batch use. +"Batch TTS" examples are sequential client loops, not one batch request. -**Campus (default)** - -| Voice ID | Name | Description | -|---|---|---| -| `xiayu` | 夏雨 | Male, balanced (default) | -| `liwa` | 丽娃 | Female, balanced | - -**Male** - -| Voice ID | Name | Description | -|---|---|---| -| `male_warm` | 温润男声 | Gentle, restrained | -| `male_steady` | 稳重学长 | Young, steady, narrative | -| `male_news` | 男声·新闻 | Standard broadcast | -| `male_philosophy` | 男声·哲理 | Slower, reflective | -| `yunze` | 云泽大叔 | Middle-aged, deep | - -**Female** - -| Voice ID | Name | Description | -|---|---|---| -| `female_sweet` | 甜美女声 | Bright, sweet, friendly | -| `female_literary` | 女声·文艺 | Gentle, literary | -| `female_news` | 女声·新闻 | Standard broadcast, brisk | - -**Dialect** - -| Voice ID | Name | Description | -|---|---|---| -| `sichuan` | 四川话 | Sichuan dialect | -| `tianjin` | 天津话 | Tianjin dialect | -| `shaanxi` | 陕西话 | Shaanxi dialect | - -**Multi-language and character** - -| Voice ID | Name | Description | -|---|---|---| -| `japanese` | 日语 | Japanese voice | -| `lindaiyu` | 林黛玉 | Classical drama character | -| `labixiaoxin` | 蜡笔小新 | Anime character | - -### TTS errors - -Invalid parameters return `400` with a JSON body containing `error`, -`request_id`, and `details`: - -```json -{ - "error": "voice 'xiaoming' not found", - "request_id": "3f9a2b1c", - "details": { - "available_voices": ["xiayu", "liwa", "male_warm"] - } -} -``` - -Common errors: `input is required`, `speed must be between 0.25 and 4.0`, -`response_format 'xxx' not supported`, `voice 'xxx' not found`. "Batch TTS" in -the official examples is a sequential client loop, not a batch request shape. - -## Model List +## Model list ```http GET https://chat.ecnu.edu.cn/open/api/v1/models ``` -There is no request body. Authentication is still required. The response is an -OpenAI-style list with `data[].id`, `object`, `created`, and `owned_by`. Treat -this endpoint as the runtime discovery surface; the example list in the docs -may lag the live service and may omit aliases or newer models. - -## Anthropic-Compatible API - -```text -ANTHROPIC_BASE_URL=https://chat.ecnu.edu.cn/open/api/anthropic -ANTHROPIC_AUTH_TOKEN= -``` - -The Anthropic SDK sends messages to the resulting `/v1/messages` path. - -### Model mapping - -| Requested model | Effective ECNU model | -|---|---| -| `ecnu-max` | `ecnu-max` | -| `ecnu-plus` | `ecnu-plus` | -| `opus` family | `ecnu-max` | -| `sonnet` family | `ecnu-plus` | -| `haiku` family | `ecnu-plus` | -| Other unrecognized model names | `ecnu-plus` | - -For Anthropic tools that inspect the model name to determine context size, pass -`ecnu-max[1m]`. The compatibility layer removes `[1m]` before routing and tells -the tool that the model supports the documented 1M-character context. Do not -generalize this suffix to the OpenAI-compatible APIs. - -### Thinking effort - -The Anthropic-compatible API supports `output_config.effort` to specify -thinking intensity. The proxy maps it to `ecnu-max` tiers: - -| Client input (`output_config.effort`) | `ecnu-max` actual tier | -|---|---| -| `minimal` | `low` | -| `low` | `low` | -| `medium` | `high` | -| `high` | `high` | -| `xhigh` | `high` | -| `max` | `max` | -| `none` | Thinking disabled | - -Thinking effort only applies to `ecnu-max`; `ecnu-plus` ignores it. Passing -`output_config.effort: "none"` disables thinking. When omitted, the server -default applies. +The documented response is an OpenAI-style list with `data[].id`, `object`, +`created`, and `owned_by`. Use it for runtime visibility, then consult the model +documentation for capabilities, aliases, and prices. Do not treat visibility +alone as a capability guarantee or an authentication check. -### Image handling in compatibility layers +## Structured output -When `ecnu-max` is called through the Anthropic or Responses compatibility -layer, the service automatically removes image content from the request to -avoid unsupported-vision errors. `ecnu-plus` retains image input normally. Do -not rely on this stripping for request validation; use `ecnu-plus` for all -image-understanding requests. - -## Structured Output - -Structured output is documented for `ecnu-plus` and its legacy alias -`ecnu-turbo`. It uses XGrammar constrained decoding through Chat Completions. +Structured output is documented for `ecnu-plus` and the legacy alias +`ecnu-turbo`. ```json { "response_format": { "type": "json_schema", "json_schema": { - "name": "info_extraction", + "name": "result", "schema": { "type": "object", "properties": { @@ -505,9 +257,34 @@ Structured output is documented for `ecnu-plus` and its legacy alias } ``` -Constraint decoding targets structural validity, not factual or semantic -correctness. Give explicit instructions and examples, include all required -fields in the schema, and allocate enough `max_tokens` for the complete value. +XGrammar constrains structure, not factual or semantic correctness. Give clear +instructions and allocate enough `max_tokens` to complete every required field. + +## Anthropic-compatible messages + +Set: + +```text +ANTHROPIC_BASE_URL=https://chat.ecnu.edu.cn/open/api/anthropic +ANTHROPIC_AUTH_TOKEN= +``` + +Documented mappings: + +| Requested name | Effective model | +|---|---| +| `ecnu-max` | `ecnu-max` | +| `ecnu-plus` | `ecnu-plus` | +| `opus` family | `ecnu-max` | +| `sonnet` or `haiku` family | `ecnu-plus` | +| other unrecognized names | `ecnu-plus` | + +The documentation describes `ecnu-max[1m]` for Anthropic tools that inspect the +model name to advertise a 1M-character context. Treat the suffix as +compatibility metadata, not a model name for OpenAI-compatible endpoints. + +`output_config.effort` controls thinking intensity for `ecnu-max`; `none` +disables thinking. `ecnu-plus` ignores this field. ## Embed iFrame @@ -515,84 +292,40 @@ fields in the schema, and allocate enough `max_tokens` for the complete value. POST https://chat.ecnu.edu.cn/open/api/embed/app ``` -This experimental integration supports JSON or URL-encoded form data. The -official page also describes an SSO-based embed mode for systems already -integrated with the campus identity service; no API is published for that -mode. - -| Field | JSON type | Required | Meaning | -|---|---|---|---| -| `client_id` | string | Yes | Developer account ID | -| `client_secret` | string | Yes | Developer account secret | -| `userid` | string | Yes | User ID; docs suggest candidate/student ID where applicable | -| `username` | string | Yes | User display name | -| `appid` | string | Yes | Assigned embed application ID | +The documented request uses `client_id`, `client_secret`, `userid`, +`username`, and `appid`. Returned tickets and URLs are credentials. Do not log +or persist them. Tickets are one-time use and expire. -The response contains `code`, `message`, `data.ticket`, `data.url`, and -`data.expire` in seconds. A ticket is one-time use. Refresh the URL before -expiry; repeated access invalidates it. Treat `client_secret`, ticket, and URL -as credentials and do not log them. +## Errors and undocumented limits -## Errors and Undocumented Limits - -| HTTP status | Meaning | Typical action | -|---|---|---| -| `401` | Missing or invalid token | Check bearer token handling | -| `403` | Client IP is not allowlisted | Check application/IP authorization | -| `422` | Request body validation failed | Inspect `detail`, field path, type, and shape | -| `429` | Quota, rate, or service protection | Stop parallel calls, inspect credits, retry later | - -`detail` may be a string or an array of validation objects. Do not assume every -error response is JSON; retain the HTTP status and a bounded body sample. - -When the docs publish no limit, write "not documented". Do not replace it with -an OpenAI default, a model-card limit, a UI limit, or a value observed once. - -## Live Verification Notes - -Observed on 2026-08-21 with a personal token against the live service. These -are point-in-time observations, not documented contracts; re-verify before -relying on them. - -- `GET /models` does not return the documented `401` for a bad token. An - invalid bearer token returns `200` with `{"object":"list","data":[]}`; a - missing Authorization header returns `500` with an HTML error page inside - the `error` field. Do not treat an empty model list as an auth check. -- The live `GET /models` list includes `ecnu-image-pro`, absent from the model - page. A probe call to `/images/generations` with that model returned - `500 Internal Server Error` as plain text, so it is listed but not - verifiably usable yet. -- TTS with an invalid `voice` returned `500 Internal Server Error` as plain - text, not the documented `400` JSON body with `details.available_voices`. -- TTS `pcm` responses set `Content-Type: audio/pcm` but did not include the - documented `Content-Rate`, `Content-Channels`, and `Content-Bits` headers. -- Anthropic messages with model `ecnu-max[1m]` returned `401` with - `{"detail":"Error code: 401 - {'detail': '获取第三方元数据失败'}"}`, while - plain `ecnu-max` requests work. The documented suffix handling may be broken - or depend on unlisted account metadata. - -Everything else verified as documented on the same date: chat completions, -thinking with `reasoning_effort` (including `reasoning_content` omission in -non-tool multi-turn), tool calling, vision content parts, structured output, -Responses API including `reasoning.effort`, embeddings (scalar and array, -1024 dims), rerank, Anthropic model mapping and `output_config.effort`, image -generation, TTS default and new voices, and the `422` validation shape. - -## Official Sources - -- Authorization: https://developer.ecnu.edu.cn/vitepress/llm/authorization.html -- Models: https://developer.ecnu.edu.cn/vitepress/llm/model.html -- Thinking: https://developer.ecnu.edu.cn/vitepress/llm/thinking.html -- Chat Completions: https://developer.ecnu.edu.cn/vitepress/llm/api/completions.html -- Responses: https://developer.ecnu.edu.cn/vitepress/llm/api/responses.html -- Vision: https://developer.ecnu.edu.cn/vitepress/llm/api/vision.html -- Embeddings: https://developer.ecnu.edu.cn/vitepress/llm/api/embedding.html -- Rerank: https://developer.ecnu.edu.cn/vitepress/llm/api/rerank.html -- Image generation: https://developer.ecnu.edu.cn/vitepress/llm/api/imagegenerate.html -- Text-to-speech: https://developer.ecnu.edu.cn/vitepress/llm/api/audio.html -- Model list: https://developer.ecnu.edu.cn/vitepress/llm/api/models.html -- Anthropic compatibility: https://developer.ecnu.edu.cn/vitepress/llm/api/anthropic.html -- Structured output: https://developer.ecnu.edu.cn/vitepress/llm/api/structuredoutput.html -- Embed iFrame: https://developer.ecnu.edu.cn/vitepress/llm/api/embediframe.html -- Local deployment and data security: https://developer.ecnu.edu.cn/vitepress/llm/security.html -- Developer agreement (token rules): https://developer.ecnu.edu.cn/vitepress/llm/tos.html +| Status | Typical meaning | +|---|---| +| `401` | Missing or invalid credentials | +| `403` | Application or client IP is not authorized | +| `422` | Request body validation failed | +| `429` | Quota, rate control, or short-term service protection | +| `5xx` | Server, proxy, or undocumented compatibility failure | + +`detail` may be a string or an array of validation objects. Preserve the HTTP +status, content type, and a bounded redacted body sample. Do not assume every +error is JSON. + +When ECNU publishes no limit, say "not documented." Do not substitute an OpenAI +default, model-card value, UI limit, or one-time observation. + +## Official sources + +- https://developer.ecnu.edu.cn/vitepress/llm/model.html +- https://developer.ecnu.edu.cn/vitepress/llm/thinking.html +- https://developer.ecnu.edu.cn/vitepress/llm/api/models.html +- https://developer.ecnu.edu.cn/vitepress/llm/api/completions.html +- https://developer.ecnu.edu.cn/vitepress/llm/api/responses.html +- https://developer.ecnu.edu.cn/vitepress/llm/api/embedding.html +- https://developer.ecnu.edu.cn/vitepress/llm/api/rerank.html +- https://developer.ecnu.edu.cn/vitepress/llm/api/imagegenerate.html +- https://developer.ecnu.edu.cn/vitepress/llm/api/audio.html +- https://developer.ecnu.edu.cn/vitepress/llm/api/anthropic.html +- https://developer.ecnu.edu.cn/vitepress/llm/api/structuredoutput.html +- https://developer.ecnu.edu.cn/vitepress/llm/api/embediframe.html +- https://developer.ecnu.edu.cn/vitepress/llm/error.html +- https://developer.ecnu.edu.cn/vitepress/llm/tos.html diff --git a/references/examples.md b/references/examples.md index e8a6a16..39651d4 100644 --- a/references/examples.md +++ b/references/examples.md @@ -1,51 +1,15 @@ # ECNU API Examples -These examples favor explicit request shapes and safe defaults. They do not -demonstrate undocumented parameters or parallel calls. - -## Table of Contents - -- [Setup](#setup) -- [Chat Completions](#chat-completions) -- [Responses API](#responses-api) -- [Thinking and Streaming](#thinking-and-streaming) -- [Tool Calling](#tool-calling) -- [Vision](#vision) -- [Embeddings](#embeddings) -- [LangChain Embeddings](#langchain-embeddings) -- [Rerank](#rerank) -- [Image Generation](#image-generation) -- [Text-to-Speech](#text-to-speech) -- [Structured Output](#structured-output) -- [Anthropic Compatibility](#anthropic-compatibility) -- [Model Discovery](#model-discovery) -- [HTTP Error Handling](#http-error-handling) -- [Sequential Workloads](#sequential-workloads) +These examples use environment variables, explicit request shapes, and +sequential calls. They avoid undocumented parameters. ## Setup -Install only the SDKs used by the selected examples: - ```bash pip install openai requests -``` - -Keep the API key in an environment variable. - -PowerShell: - -```powershell -$env:ECNU_API_KEY = "your-api-key" -``` - -macOS or Linux: - -```bash export ECNU_API_KEY="your-api-key" ``` -Create the OpenAI-compatible client: - ```python import os from openai import OpenAI @@ -72,13 +36,8 @@ completion = client.chat.completions.create( print(completion.choices[0].message.content) ``` -The documented roles are `system`, `user`, and `assistant`. Keep -`temperature` and `top_p` between 0 and 1 when setting them explicitly. - ## Responses API -Both primary dialog models support the OpenAI Responses format: - ```python response = client.responses.create( model="ecnu-max", @@ -88,33 +47,12 @@ response = client.responses.create( print(response.output_text) ``` -The ECNU page does not publish a complete list of supported OpenAI Responses -tools or event types. Start with text input and verify advanced features before -depending on them. +Start with text input. Verify advanced Responses tools and streaming events +before depending on them. -## Thinking and Streaming +## Thinking mode -`thinking` is an ECNU request extension. Pass it through `extra_body`: - -```python -completion = client.chat.completions.create( - model="ecnu-max", - messages=[{"role": "user", "content": "Analyze this problem."}], - extra_body={"thinking": {"type": "enabled"}}, -) - -message = completion.choices[0].message -reasoning = getattr(message, "reasoning_content", None) -if reasoning: - print("Reasoning:", reasoning) -print("Answer:", message.content) -``` - -### Reasoning Effort - -`ecnu-max` supports `reasoning_effort` to control thinking intensity. It -accepts `low`, `high`, or `max`, and only takes effect when thinking is -enabled. `ecnu-plus` ignores this parameter: +Pass ECNU extensions through `extra_body`: ```python completion = client.chat.completions.create( @@ -127,19 +65,14 @@ completion = client.chat.completions.create( ) message = completion.choices[0].message -reasoning = getattr(message, "reasoning_content", None) -if reasoning: - print("Reasoning:", reasoning) -print("Answer:", message.content) +answer = message.content +print(answer) ``` -Higher intensity produces more thorough reasoning but increases latency and -token consumption. When thinking is enabled, `temperature` and `top_p` may not -take effect or may be restricted; prefer defaults. - -### Streaming +Do not print hidden reasoning in user-facing applications. Preserve +`reasoning_content` only when required for a subsequent tool-using turn. -Stream text deltas: +## Streaming ```python stream = client.chat.completions.create( @@ -154,7 +87,7 @@ for chunk in stream: print(delta, end="", flush=True) ``` -## Tool Calling +## Tool calling ```python tools = [ @@ -166,10 +99,7 @@ tools = [ "parameters": { "type": "object", "properties": { - "location": { - "type": "string", - "description": "City name, such as Shanghai.", - } + "location": {"type": "string"}, }, "required": ["location"], }, @@ -183,18 +113,15 @@ completion = client.chat.completions.create( tools=tools, ) -message = completion.choices[0].message -for call in message.tool_calls or []: +for call in completion.choices[0].message.tool_calls or []: print(call.id, call.function.name, call.function.arguments) ``` -The caller must execute the function and send the result back in a subsequent -message. ECNU does not execute user-defined functions for the caller. +The caller must execute the function and submit the tool result in a subsequent +turn. ## Vision -Use `ecnu-plus` for new integrations. A public image URL: - ```python completion = client.chat.completions.create( model="ecnu-plus", @@ -215,46 +142,12 @@ completion = client.chat.completions.create( print(completion.choices[0].message.content) ``` -A local image as a base64 data URL: - -```python -import base64 -from pathlib import Path - -image_bytes = Path("image.jpg").read_bytes() -image_b64 = base64.b64encode(image_bytes).decode("ascii") - -completion = client.chat.completions.create( - model="ecnu-plus", - messages=[ - { - "role": "user", - "content": [ - {"type": "text", "text": "Describe this image."}, - { - "type": "image_url", - "image_url": { - "url": f"data:image/jpeg;base64,{image_b64}" - }, - }, - ], - } - ], -) -``` - -The API docs publish no image-count, byte-size, or pixel limit. Do not encode a -web UI upload limit as an API validation rule. +For a local image, base64-encode it into a data URL. Do not assume undocumented +image-count, byte-size, or pixel limits. ## Embeddings -The key distinction is JSON type: - -- One text: `input="..."` -- Multiple texts in one HTTP request: `input=["...", "..."]` -- Unsupported: integer token IDs such as `input=[123, 456]` - -### One text +One text: ```python response = client.embeddings.create( @@ -264,64 +157,32 @@ response = client.embeddings.create( vector = response.data[0].embedding assert len(vector) == 1024 -print(response.data[0].index, len(vector)) ``` -### String array +Several texts in one request: ```python texts = [ "华东师范大学是综合性研究型大学。", "量子计算是计算科学的前沿领域。", - "求实创造,为人师表。", ] if not texts or not all(isinstance(text, str) for text in texts): - raise TypeError("Embedding input must be a non-empty string array") + raise TypeError("input must be a non-empty string array") response = client.embeddings.create( model="ecnu-embedding-small", input=texts, ) -vectors_by_index = { - item.index: item.embedding - for item in response.data -} - -for index, text in enumerate(texts): - vector = vectors_by_index[index] - assert len(vector) == 1024 - print(index, text[:20], len(vector)) -``` - -The published limit is 8192 characters, but the official page does not say -whether an array is checked per element, by combined characters, or both. It -also publishes no maximum item count. Keep batches conservative and split a -batch on `422` rather than claiming a guessed maximum. - -For many texts, call sequential batches: - -```python -def chunks(items, size): - for start in range(0, len(items), size): - yield items[start:start + size] - - -all_vectors = [] -for batch in chunks(texts, size=16): # Client policy, not an ECNU limit. - response = client.embeddings.create( - model="ecnu-embedding-small", - input=batch, - ) - ordered = sorted(response.data, key=lambda item: item.index) - all_vectors.extend(item.embedding for item in ordered) +ordered = sorted(response.data, key=lambda item: item.index) +vectors = [item.embedding for item in ordered] +assert all(len(vector) == 1024 for vector in vectors) ``` -Do not run these batches concurrently. The size `16` is an application choice -for conservative requests, not a documented platform maximum. +Do not send integer token IDs. -## LangChain Embeddings +### LangChain embeddings ```bash pip install langchain-openai @@ -334,7 +195,6 @@ embeddings = OpenAIEmbeddings( base_url="https://chat.ecnu.edu.cn/open/api/v1", api_key=api_key, model="ecnu-embedding-small", - dimensions=1024, check_embedding_ctx_length=False, ) @@ -342,14 +202,31 @@ vector = embeddings.embed_query("Hello world") assert len(vector) == 1024 ``` -`check_embedding_ctx_length=False` is required because LangChain otherwise may -convert strings into OpenAI token IDs. ECNU accepts strings, not OpenAI token -arrays. `dimensions=1024` describes the fixed output size; it does not request -an alternative size from ECNU. +`check_embedding_ctx_length=False` prevents LangChain from converting strings +to OpenAI token IDs. Validate the fixed output size after the response; do not +send a dimension-selection request field that ECNU does not document. + +For many texts, use conservative sequential batches: + +```python +def chunks(items, size): + for start in range(0, len(items), size): + yield items[start : start + size] + + +all_vectors = [] +for batch in chunks(texts, size=16): # Application policy, not an ECNU limit. + response = client.embeddings.create( + model="ecnu-embedding-small", + input=batch, + ) + ordered = sorted(response.data, key=lambda item: item.index) + all_vectors.extend(item.embedding for item in ordered) +``` ## Rerank -Use direct HTTP because the OpenAI SDK has no rerank resource: +The OpenAI SDK has no rerank resource, so use direct HTTP: ```python import requests @@ -357,16 +234,15 @@ import requests documents = [ "华东师范大学是教育部直属的综合性研究型大学。", "量子计算是计算科学的前沿领域。", - "学校校训是求实创造,为人师表。", ] -top_n = 3 +top_n = 2 if not documents or not all(isinstance(doc, str) for doc in documents): raise TypeError("documents must be a non-empty string array") if any(len(doc) > 8192 for doc in documents): - raise ValueError("Each rerank document must be at most 8192 characters") + raise ValueError("each document must be at most 8192 characters") if not 1 <= top_n <= len(documents): - raise ValueError("Application policy requires 1 <= top_n <= document count") + raise ValueError("application policy requires a valid result count") response = requests.post( "https://chat.ecnu.edu.cn/open/api/v1/rerank", @@ -383,17 +259,17 @@ response = requests.post( }, timeout=60, ) - response.raise_for_status() + for result in response.json()["results"]: print(result["index"], result["relevance_score"]) - print(result.get("document", "")) ``` -The `top_n <= document count` check is sensible client logic, not a published -ECNU maximum. The service docs specify only the default `top_n=5`. +The local `top_n` check is application policy, not a published ECNU maximum. + +## Image generation -## Image Generation +This call consumes credits. Do not run it merely to validate code. ```python response = client.images.generate( @@ -406,14 +282,12 @@ response = client.images.generate( print(response.data[0].url) ``` -Prompts are limited to 1024 characters and may be compressed over 500 -characters. URL results expire after 24 hours; download or transfer them -immediately. +Transfer URL results before their 24-hour expiry. Do not blindly retry after an +ambiguous timeout. -Supported sizes are `512x512`, `768x768`, `720x1280`, `1280x720`, and -`1024x1024`. +## Text-to-speech -## Text-to-Speech +This call consumes credits: ```python response = client.audio.speech.create( @@ -427,31 +301,10 @@ response = client.audio.speech.create( response.stream_to_file("output.mp3") ``` -Input is limited to 4096 characters. The model page states that the underlying -model was updated to Fun-CosyVoice3-0.5B with 16 voice types; see the API -reference for the full voice list. Speed is 0.25 through 4.0. Multiple texts -require separate sequential API calls: +Multiple texts require separate sequential calls. They are not one batch API +request. -```python -jobs = [ - ("第一段文本。", "xiayu"), - ("第二段文本。", "female_sweet"), - ("第三段文本。", "male_news"), -] - -for index, (text, voice) in enumerate(jobs, start=1): - response = client.audio.speech.create( - model="ecnu-tts", - input=text, - voice=voice, - response_format="mp3", - ) - response.stream_to_file(f"speech-{index}.mp3") -``` - -This loop is not a batch request; each iteration consumes one TTS call. - -## Structured Output +## Structured output ```python import json @@ -471,12 +324,12 @@ completion = client.chat.completions.create( messages=[ { "role": "system", - "content": ( - "Extract name, department, and title. " - "Return values matching the supplied schema." - ), + "content": "Extract name, department, and title.", + }, + { + "role": "user", + "content": "张三,法律事务部高级总监。", }, - {"role": "user", "content": "张三,法律事务部高级总监。"}, ], response_format={ "type": "json_schema", @@ -492,25 +345,10 @@ result = json.loads(completion.choices[0].message.content) print(result) ``` -XGrammar constrains structure, not meaning. Keep explicit instructions and -enough `max_tokens` for all required fields. - -## Anthropic Compatibility +## Anthropic compatibility ```bash pip install anthropic -``` - -PowerShell: - -```powershell -$env:ANTHROPIC_BASE_URL = "https://chat.ecnu.edu.cn/open/api/anthropic" -$env:ANTHROPIC_AUTH_TOKEN = $env:ECNU_API_KEY -``` - -macOS or Linux: - -```bash export ANTHROPIC_BASE_URL="https://chat.ecnu.edu.cn/open/api/anthropic" export ANTHROPIC_AUTH_TOKEN="$ECNU_API_KEY" ``` @@ -523,7 +361,6 @@ anthropic_client = anthropic.Anthropic() message = anthropic_client.messages.create( model="ecnu-plus", max_tokens=1000, - system="You are a helpful assistant.", messages=[ { "role": "user", @@ -535,47 +372,31 @@ message = anthropic_client.messages.create( print(message.content) ``` -For an Anthropic tool that relies on the model name to recognize the larger -context window: +Use plain `ecnu-max` by default. Only try `ecnu-max[1m]` when an Anthropic tool +must recognize the long-context suffix: ```python -message = anthropic_client.messages.create( - model="ecnu-max[1m]", - max_tokens=1000, - messages=[{"role": "user", "content": "Summarize the long context."}], -) -``` - -The suffix is specific to the Anthropic compatibility layer. `opus` names map -to `ecnu-max`; `sonnet`, `haiku`, and other unrecognized names map to -`ecnu-plus`. - -The Anthropic-compatible API also supports `output_config.effort` to control -thinking intensity for `ecnu-max`. The proxy maps `minimal`/`low` to `low`, -`medium`/`high`/`xhigh` to `high`, `max` to `max`, and `none` to thinking -disabled. - -The Responses-compatible API supports `reasoning.effort` with the same tier -mapping. Passing `reasoning.effort: "none"` disables thinking. - -When `ecnu-max` is called through either compatibility layer, the service -automatically strips image content from the request. Use `ecnu-plus` for -vision tasks. - -## Model Discovery - -```python -models = client.models.list() -for model in models.data: - print(model.id, model.owned_by) +def create_long_context_message(client, messages): + try: + return client.messages.create( + model="ecnu-max[1m]", + max_tokens=1000, + messages=messages, + ) + except anthropic.AuthenticationError: + return client.messages.create( + model="ecnu-max", + max_tokens=1000, + messages=messages, + ) ``` -Use the response to discover current IDs, then use the model documentation to -interpret aliases, vision support, thinking defaults, and pricing. +Log the fallback without logging prompts or credentials. The fallback preserves +model access but may not advertise the same context capability to the client. -## HTTP Error Handling +## Error handling -`detail` may be a string or a validation-error array. Preserve both forms: +Preserve string and array forms of `detail`, and tolerate non-JSON errors: ```python def raise_ecnu_error(response): @@ -592,36 +413,11 @@ def raise_ecnu_error(response): else: detail = response.text[:1000] - raise RuntimeError(f"ECNU API HTTP {response.status_code}: {detail}") -``` - -Interpret common statuses before retrying: - -- `401`: fix token handling; do not retry unchanged credentials. -- `403`: verify the application's IP allowlist. -- `422`: inspect JSON field type and `detail[].loc`; splitting a genuinely - oversized batch may help, but blind retry does not. -- `429`: stop concurrent calls, inspect credits, then retry later with backoff. - -## Sequential Workloads - -Do not use a thread pool or `asyncio.gather` for ECNU batches. Process one -request at a time and keep enough information to resume safely: - -```python -results = [] -for index, prompt in enumerate(prompts): - completion = client.chat.completions.create( - model="ecnu-plus", - messages=[{"role": "user", "content": prompt}], - ) - results.append( - { - "index": index, - "content": completion.choices[0].message.content, - } + raise RuntimeError( + f"ECNU API HTTP {response.status_code}: {detail}" ) ``` -Sequential calls improve stability and make `429` recovery, credit accounting, -and partial-result persistence easier to reason about. +Do not retry unchanged credentials after `401`. For `422`, inspect field paths +and types. For `429`, stop concurrency, inspect credits, then use bounded +backoff. Do not blindly retry billable calls after an ambiguous timeout. diff --git a/references/known_deviations.md b/references/known_deviations.md new file mode 100644 index 0000000..16339c3 --- /dev/null +++ b/references/known_deviations.md @@ -0,0 +1,52 @@ +# Known Service Deviations + +These are point-in-time observations from a personal-token test on +**2026-08-21**. They are not official contracts. Re-run a controlled smoke test +before relying on them, and update this file only when a result is actually +reproduced. + +## Observation matrix + +| Area | Documented expectation | Observed behavior | Application fallback | +|---|---|---|---| +| `/models`, invalid token | authentication error | `200` with an empty model list | never use an empty list as an authentication check | +| `/models`, missing auth | authentication error | `500` with an HTML error embedded in the response | preserve content type/body; report auth as inconclusive | +| runtime model list | documented models | included `ecnu-image-pro`, absent from model page | do not select undocumented IDs without an endpoint probe | +| `ecnu-image-pro` generation | not documented | probe returned plain-text `500` | use documented `ecnu-image` | +| TTS invalid voice | documented `400` JSON details | plain-text `500` | preserve status/content type; do not assume JSON | +| TTS PCM metadata | documented format metadata headers | `Content-Rate`, `Content-Channels`, and `Content-Bits` absent | inspect headers and require caller-side audio configuration | +| Anthropic `ecnu-max[1m]` | suffix stripped and long context advertised | `401` third-party metadata failure; plain `ecnu-max` worked | fall back to plain `ecnu-max` and report context-advertising difference | + +## Verified in the same 2026-08-21 run + +The repository maintainer recorded successful checks for: + +- Chat Completions; +- thinking with `reasoning_effort`; +- non-tool multi-turn handling; +- tool calling; +- vision content parts; +- structured output; +- Responses API and `reasoning.effort`; +- scalar and array embeddings with 1024-value outputs; +- rerank; +- Anthropic model mapping and `output_config.effort`; +- documented image generation; +- default and additional TTS voices; +- `422` validation response shape. + +These statements remain dated observations. They are not substitutes for +current production monitoring. + +## How to update this file + +1. Run a controlled authenticated test. +2. Record UTC date, account type, Python/SDK version, endpoint, status, content + type, and structural result. +3. Remove keys, prompts, generated content, one-time URLs, and personal data. +4. Reproduce an anomaly before replacing an existing observation. +5. Keep the documented expectation in `api_reference.md`; keep only the + deviation here. +6. Add or update an application fallback. +7. Do not write "fixed" merely because one subsequent request succeeded; note + the evidence and date. diff --git a/references/models.md b/references/models.md index 7e7cccf..ee86b70 100644 --- a/references/models.md +++ b/references/models.md @@ -1,219 +1,81 @@ -# ECNU Models, Credits, Quotas, and Errors - -## Table of Contents - -- [Source Precedence](#source-precedence) -- [Primary Dialog Models](#primary-dialog-models) -- [Compatibility Aliases](#compatibility-aliases) -- [Specialized Models](#specialized-models) -- [Deployment and Availability](#deployment-and-availability) -- [Thinking Mode](#thinking-mode) -- [Shared Credits Quotas](#shared-credits-quotas) -- [Dialog Credits Calculation](#dialog-credits-calculation) -- [Fixed-Cost Capabilities](#fixed-cost-capabilities) -- [Official Token Equivalents](#official-token-equivalents) -- [Errors](#errors) -- [Recent Changes](#recent-changes) -- [Official Sources](#official-sources) - -## Source Precedence - -ECNU documentation pages are not always updated together. Resolve conflicts in -this order: - -1. Use the current model page for model identity, context figures, aliases, and - capabilities. -2. Use the endpoint page for JSON fields, field types, and endpoint-specific - limits. -3. Use the quota page for current prices, cache treatment, and quota periods. -4. Use release notes to understand when a change occurred, not to override a - newer current-state page. -5. Use `GET /models` for runtime discovery, while remembering that aliases and - capabilities may require the documentation for interpretation. - -Example: the former vision page now redirects to the completions page's -multimodal section, and the model page defines `ecnu-vl` as a compatibility -alias for `ecnu-plus`. New integrations should use `ecnu-plus`. - -## Primary Dialog Models - -| Model | Underlying model | Published context | Thinking | Tools | Vision | Positioning | -|---|---|---|---|---|---|---| -| `ecnu-max` | [DeepSeek-V4-Flash-0731](https://modelscope.cn/models/deepseek-ai/DeepSeek-V4-Flash-0731) | 1M | Supported, default off | Yes | No | Flagship for complex text and code tasks | -| `ecnu-plus` | [Qwen3.6-27B](https://modelscope.cn/models/Qwen/Qwen3.6-27B) | 256K | Supported, default off | Yes | Yes | General-purpose balance of quality, cost, and latency | - -The model table labels the context only as `1M` and `256K`; it does not specify -tokens or characters. Preserve those figures without adding a unit. The -Anthropic compatibility page separately calls `ecnu-max[1m]` a 1M-character -context signal for Anthropic tools. - -Use `ecnu-plus` for all new image-understanding integrations. `ecnu-max` no -longer supports vision after its DeepSeek-V4-Flash-0731 upgrade. - -## Compatibility Aliases - -Prefer `ecnu-max` and `ecnu-plus` for new integrations. - -| Historical request model | Effective behavior | Note | -|---|---|---| -| `ecnu-reasoner` | `ecnu-max` plus thinking enabled | Thinking defaults on | -| `ecnu-reasoner-lite` | `ecnu-plus` plus thinking enabled | Thinking defaults on | -| `ecnu-turbo` | `ecnu-plus` | Legacy alias; still used in structured-output docs | -| `ecnu-vl` | `ecnu-plus` | Legacy vision alias | -| `InnoSpark` | `ecnu-plus` | Legacy alias | -| `educhat-r1` | `ecnu-plus` | Legacy alias | -| `educhat-general` | `ecnu-plus` | Legacy alias | -| `educhat-psychology` | `ecnu-plus` | Legacy alias | -| `ChatECNU` | `ecnu-plus` | Legacy alias | -| `gpt-4` | `ecnu-plus` | Compatibility name | - -For Anthropic-compatible requests, mapping is broader: - -| Anthropic request name | Effective model | -|---|---| -| `opus` family | `ecnu-max` | -| `sonnet` or `haiku` family | `ecnu-plus` | -| Other unrecognized names | `ecnu-plus` | - -Use `ecnu-max[1m]` only with Anthropic tools that inspect the model name for -context capability. The compatibility layer strips the suffix before routing. - -## Specialized Models - -| Model | Underlying model | Request contract | Output or capability | -|---|---|---|---| -| `ecnu-embedding-small` | [bge-m3](https://modelscope.cn/models/BAAI/bge-m3) | `input` is one string or a string array; published limit 8192 characters with batch scope unspecified | 1024-float embeddings | -| `ecnu-rerank` | [bge-reranker-v2-m3](https://modelscope.cn/models/BAAI/bge-reranker-v2-m3) | `documents` is `string[]`; each document at most 8192 characters | Ranked indices and relevance scores | -| `ecnu-image` | [Z-Image-Turbo](https://modelscope.cn/models/Tongyi-MAI/Z-Image-Turbo) | Prompt at most 1024 characters; prompts over 500 may be compressed | Image URL or base64 | -| `ecnu-tts` | [Fun-CosyVoice3-0.5B](https://modelscope.cn/models/FunAudioLLM/Fun-CosyVoice3-0.5B-2512) | Input at most 4096 characters | Binary audio | - -The model page calls embedding and rerank context `8K`, while their endpoint -pages express limits in characters. Use the endpoint wording for request -validation; do not convert 8192 characters to 8192 tokens. - -### TTS voices - -`ecnu-tts` supports 16 voice types after the Fun-CosyVoice3-0.5B upgrade on -2026-08-03. The endpoint page documents all of them: - -**Campus (default)** - -| Voice ID | Name | Description | Use case | -|---|---|---|---| -| `xiayu` | 夏雨 | Male, balanced (default) | General | -| `liwa` | 丽娃 | Female, balanced | General | - -**Male** - -| Voice ID | Name | Description | Use case | -|---|---|---|---| -| `male_warm` | 温润男声 | Gentle, restrained | Emotional narration, audiobooks | -| `male_steady` | 稳重学长 | Young, steady, narrative | Lectures, campus promos | -| `male_news` | 男声·新闻 | Standard broadcast | News, announcements | -| `male_philosophy` | 男声·哲理 | Slower, reflective | Commentary, essay reading | -| `yunze` | 云泽大叔 | Middle-aged, deep | Documentary, science narration | +# ECNU Models, Credits, and Quotas -**Female** +Verify time-sensitive values against the official model and quota pages before +a production decision. -| Voice ID | Name | Description | Use case | -|---|---|---|---| -| `female_sweet` | 甜美女声 | Bright, sweet, friendly | Customer service, guides | -| `female_literary` | 女声·文艺 | Gentle, literary | Prose reading, brand copy | -| `female_news` | 女声·新闻 | Standard broadcast, brisk | News, announcements | +## Source precedence -**Dialect** +1. Current model page: identity, context figures, aliases, and capabilities. +2. Endpoint page: request fields, types, and endpoint-specific limits. +3. Quota page: current prices, cache treatment, and quota periods. +4. Release notes: change dates. +5. `GET /models`: runtime visibility only. -| Voice ID | Name | Description | Use case | -|---|---|---|---| -| `sichuan` | 四川话 | Sichuan dialect | Dialect content | -| `tianjin` | 天津话 | Tianjin dialect | Dialect content | -| `shaanxi` | 陕西话 | Shaanxi dialect | Dialect content | +## Primary dialog models -**Multi-language and character** +| Model | Underlying model | Published context | Thinking | Tools | Vision | +|---|---|---|---|---|---| +| `ecnu-max` | DeepSeek-V4-Flash-0731 | 1M | Supported, default off | Yes | No | +| `ecnu-plus` | Qwen3.6-27B | 256K | Supported, default off | Yes | Yes | -| Voice ID | Name | Description | Use case | -|---|---|---|---| -| `japanese` | 日语 | Japanese voice | Japanese content | -| `lindaiyu` | 林黛玉 | Classical drama character | Role voice, fun content | -| `labixiaoxin` | 蜡笔小新 | Anime character | Role voice, fun content | +The model table does not label `1M` and `256K` as tokens or characters. Preserve +the published figures without adding a unit. The Anthropic page separately +describes `ecnu-max[1m]` as a 1M-character compatibility signal. -Dialect and character voices are trained on specific corpora; long written -passages may produce unstable accent or tone. Test with short text before batch -use. +Use `ecnu-plus` for image understanding. Use `ecnu-max` for complex text and +code where its higher cost and latency are justified. -Supported audio formats: `mp3` (default), `opus`, `aac`, `flac`, `wav`, `pcm`. +## Compatibility aliases -## Deployment and Availability +Prefer the primary names for new integrations. -ECNU states that listed models are deployed on campus and requests normally -remain on campus servers. During upgrades, failures, or heavy load, ECNU may -temporarily use cloud models to preserve continuity. The dedicated security -page confirms local deployment covers all dialog, embedding/rerank, image, and -TTS models. - -ChatECNU, the Agent platform, and other campus-specific AI applications use -separate service clusters. Their behavior or availability is therefore not a -direct measurement of a personal API token's endpoint. Businesses with strict -stability requirements are invited to contact ECNU separately. +| Historical name | Effective behavior | +|---|---| +| `ecnu-reasoner` | `ecnu-max` with thinking enabled | +| `ecnu-reasoner-lite` | `ecnu-plus` with thinking enabled | +| `ecnu-turbo` | `ecnu-plus` | +| `ecnu-vl` | `ecnu-plus` | +| `InnoSpark` | `ecnu-plus` | +| `educhat-r1` | `ecnu-plus` | +| `educhat-general` | `ecnu-plus` | +| `educhat-psychology` | `ecnu-plus` | +| `ChatECNU` | `ecnu-plus` | +| `gpt-4` | `ecnu-plus` | + +Anthropic mappings are broader: `opus` maps to `ecnu-max`; `sonnet` and `haiku` +map to `ecnu-plus`; other unrecognized names map to `ecnu-plus`. + +## Specialized models + +| Model | Underlying model | Contract | +|---|---|---| +| `ecnu-embedding-small` | bge-m3 | Raw string or string array; 1024-float output | +| `ecnu-rerank` | bge-reranker-v2-m3 | String documents; 8192 characters per document | +| `ecnu-image` | Z-Image-Turbo | Prompt at most 1024 characters | +| `ecnu-tts` | Fun-CosyVoice3-0.5B | Input at most 4096 characters | -Check current availability at https://chat.ecnu.edu.cn/status. Avoid parallel -API calls; short bursts can still trigger service protection even though the -old minute-level quota was removed. +The model page labels embedding and rerank context as `8K`, while endpoint pages +express request limits in characters. Use the endpoint wording when validating +requests. -## Thinking Mode +## Thinking mode -All current dialog models support the `thinking` object: +Enable or disable dialog thinking with: ```json {"thinking":{"type":"enabled"}} ``` -or: - ```json {"thinking":{"type":"disabled"}} ``` -With the OpenAI Python SDK, pass the object through `extra_body`. A response may -include `reasoning_content`; callers must tolerate its absence. - -### Reasoning Effort - -`ecnu-max` additionally supports `reasoning_effort` to control thinking -intensity. The parameter accepts `low`, `high`, or `max`. It only takes effect -when thinking is enabled and only applies to `ecnu-max`; `ecnu-plus` ignores -it. If thinking is disabled, `reasoning_effort` has no effect. - -```json -{ - "model": "ecnu-max", - "thinking": {"type": "enabled"}, - "reasoning_effort": "high", - "messages": [{"role": "user", "content": "Analyze this problem."}] -} -``` - -Higher intensity produces more thorough reasoning but increases latency and -token consumption. When thinking is enabled without an explicit effort value, -the default follows model preference; specify `reasoning_effort` explicitly -for stable results. When thinking is enabled, `temperature` and `top_p` may -not take effect or may be restricted; prefer defaults. +For Chat Completions, `ecnu-max` accepts `reasoning_effort` values `low`, `high`, +or `max` when thinking is enabled. `ecnu-plus` ignores the field. -### Multi-turn Splicing +Anthropic-compatible requests use `output_config.effort`: -In multi-turn conversations under thinking mode: - -- If the assistant did not call a tool, its `reasoning_content` can be omitted - from subsequent context. -- If the assistant called a tool, its `reasoning_content` must be included in - all subsequent turns; some models return `400` if it is missing. - -### Anthropic-Compatible Thinking Effort - -The Anthropic-compatible API uses `output_config.effort` instead of -`reasoning_effort`. The proxy maps Anthropic levels to `ecnu-max` tiers: - -| Client input (`output_config.effort`) | `ecnu-max` actual tier | +| Client value | `ecnu-max` tier | |---|---| | `minimal` | `low` | | `low` | `low` | @@ -221,27 +83,19 @@ The Anthropic-compatible API uses `output_config.effort` instead of | `high` | `high` | | `xhigh` | `high` | | `max` | `max` | -| `none` | Thinking disabled | - -Passing `output_config.effort: "none"` disables thinking. When the parameter is -omitted, the server default applies. - -### Responses-API Thinking Effort - -The Responses-compatible API supports `reasoning.effort` for `ecnu-max`. -Passing `reasoning.effort: "none"` disables thinking; when omitted, the server -default applies. The proxy applies the same tier mapping as the Anthropic -compatible API. +| `none` | thinking disabled | -### Compatibility Layer Image Handling +Responses-compatible requests use `reasoning.effort` with the same +compatibility mapping. -When `ecnu-max` is called through the Anthropic or Responses compatibility -layer, the service automatically removes image content from the request to -avoid unsupported-vision errors. `ecnu-plus` retains image input normally. +In tool-using multi-turn thinking conversations, preserve the assistant's +`reasoning_content` when the service requires it for continuation. Do not show +hidden reasoning to end users. -## Shared Credits Quotas +## Shared credits quotas -Personal tokens share one credits pool across all models and capabilities. +The repository documentation available on 2026-08-22 records these defaults +for personal tokens: | Period | Default quota | |---|---| @@ -249,51 +103,25 @@ Personal tokens share one credits pool across all models and capabilities. | Daily | 5000 credits | | Monthly | 50000 credits | -Minute-level quota enforcement has been removed, but abnormal high-frequency -traffic can still trigger service protection. Production systems serving ECNU -users may receive independent quota pools. The official contact for higher -requirements is `dataservice@ecnu.edu.cn`. +Minute-level quota enforcement was reported as removed, but abnormal +high-frequency traffic may still trigger service protection. Recheck the quota +page before relying on these values. -## Dialog Credits Calculation +## Dialog credits -Dialog models distinguish cache-miss input, cache-hit input, and output or -thinking tokens. Cached input currently costs one fifth of uncached input. +Cached input is documented as costing one fifth of uncached input. -| Model | Input miss | Input hit | Output/thinking | +| Model | Input miss | Input hit | Output or thinking | |---|---|---|---| -| `ecnu-plus` | 100 credits / 1M tokens | 20 credits / 1M tokens | 400 credits / 1M tokens | -| `ecnu-max` | 300 credits / 1M tokens | 60 credits / 1M tokens | 1200 credits / 1M tokens | +| `ecnu-plus` | 100 credits / 1M tokens | 20 / 1M | 400 / 1M | +| `ecnu-max` | 300 credits / 1M tokens | 60 / 1M | 1200 / 1M | -Formulas: +Do not assume a cache-hit ratio. ECNU's published examples use an assumption; +it is not a guarantee for an application. -```text -ecnu-plus = input_miss / 1M * 100 - + input_hit / 1M * 20 - + output / 1M * 400 -``` - -```text -ecnu-max = input_miss / 1M * 300 - + input_hit / 1M * 60 - + output / 1M * 1200 -``` - -Do not calculate all input at the cache-miss rate when cache usage is known. -Do not promise a cache-hit ratio; ECNU uses 90% only for its published examples. - -Official examples assuming a 90% input cache-hit rate: - -| Request | `ecnu-plus` | `ecnu-max` | -|---|---|---| -| 1,500 input + 800 output | 0.36 credits | 1.09 credits | -| 10K input + 2K output | 1.08 credits | 3.24 credits | -| 100K input + 2K output | 3.6 credits | 10.8 credits | -| 500K input + 5K output | 16 credits | 48 credits | -| 1M input + 10K output | 32 credits | 96 credits | +## Fixed-cost capabilities -## Fixed-Cost Capabilities - -These calls are currently priced per call rather than per token: +The repository documentation available on 2026-08-22 records: | Capability | Model | Cost | |---|---|---| @@ -302,77 +130,43 @@ These calls are currently priced per call rather than per token: | Image generation | `ecnu-image` | 30 credits / successful generation | | Text-to-speech | `ecnu-tts` | 5 credits / call | -The per-call price does not define a supported batch size. In particular, the -embedding docs allow a string array but publish no maximum item count or total -character rule. Do not maximize batches based only on billing. - -## Official Token Equivalents - -The quota page publishes these rough equivalents assuming 90% of input tokens -are cache hits. They are estimates, not guaranteed capacity. - -| Quota | `ecnu-plus` input-only | `ecnu-plus` 4:1 mix | `ecnu-max` input-only | `ecnu-max` 4:1 mix | -|---|---|---|---|---| -| 2000 credits / 5h | ~71.43M input tokens | ~19.53M total tokens | ~23.81M input tokens | ~6.51M total tokens | -| 5000 credits / day | ~179M input tokens | ~48.83M total tokens | ~59.52M input tokens | ~16.28M total tokens | -| 50000 credits / month | ~1.786B input tokens | ~488M total tokens | ~595M input tokens | ~163M total tokens | +A per-call price does not define a supported batch size. Do not maximize a +batch based on billing alone. -Input-heavy document, codebase, and RAG workloads may be closer to the -input-only estimate only when their cache behavior resembles the assumption. - -## Errors - -| Status | Meaning | Typical cause | -|---|---|---| -| `401` | Unauthorized | Token missing or invalid | -| `403` | Forbidden | Client IP is not in the third-party allowlist | -| `422` | Request validation failed | Required field missing, wrong JSON type, unsupported shape | -| `429` | Too many requests | Quota exhausted, rate control, or short-term service protection | +## Deployment and data handling -`detail` can be a string: +The model page states that listed models are deployed locally and that data +processing normally occurs on campus servers. It also distinguishes +campus-specific applications such as ChatECNU and the Agent platform from +personal API service clusters. -```json -{"detail":"无效的令牌"} -``` - -or an array of validation errors: - -```json -{ - "detail": [ - { - "type": "missing", - "loc": ["body", "model"], - "msg": "Field required" - } - ] -} -``` +The developer agreement makes the developer responsible for token protection, +lawful handling of personal information, downstream application behavior, and +rights to submitted inputs. It also states that de-identified input and output +may be used for service optimization, statistics, troubleshooting, and safety +risk control under the agreement's conditions. -For `422`, report the `loc`, `type`, and `msg` rather than reducing every error -to a generic invalid request. For `429`, stop concurrent retries and inspect -credits before applying backoff. +Before sending personal, confidential, or regulated information, verify that +the intended use and data-handling basis are appropriate. -## Recent Changes +## Important changes | Date | Change | |---|---| -| 2026-08-10 (v3.2.1) | `ecnu-max` supports `reasoning_effort`; Anthropic API supports `output_config.effort`; Responses API supports `reasoning.effort`; compatibility layer auto-removes images from `ecnu-max` requests; bug fixes for streaming quota errors and empty-stream handling | -| 2026-08-09 | Docs: model page adds default-parameter guidance (`temperature`/`top_p` per underlying model docs) and a local deployment & data security section; security and developer-agreement (tos) pages published, including the 90-day default token validity; vision page folded into the completions page multimodal section | -| 2026-08-03 (v3.2.0) | `ecnu-tts` updated to Fun-CosyVoice3-0.5B; 16 voice types added; DeepSeek-V4-Flash-0731 Day0 deployment | -| 2026-08-01 | `ecnu-max` updated to DeepSeek-V4-Flash-0731 | -| 2026-04-24 | `ecnu-max` announced upgrade to DeepSeek-V4-Flash; vision support removed | -| 2026-04-03 | Dialog models unified to `ecnu-max` and `ecnu-plus`; `thinking` parameter introduced | -| 2025-03-20 | Native `search_mode` web search removed | - -## Official Sources - -- Models and aliases: https://developer.ecnu.edu.cn/vitepress/llm/model.html -- Thinking mode: https://developer.ecnu.edu.cn/vitepress/llm/thinking.html -- Quotas and prices: https://developer.ecnu.edu.cn/vitepress/llm/limit.html -- Errors: https://developer.ecnu.edu.cn/vitepress/llm/error.html -- Anthropic compatibility: https://developer.ecnu.edu.cn/vitepress/llm/api/anthropic.html -- Release notes: https://developer.ecnu.edu.cn/vitepress/llm/release.html -- Local deployment and data security: https://developer.ecnu.edu.cn/vitepress/llm/security.html -- Developer agreement (token rules): https://developer.ecnu.edu.cn/vitepress/llm/tos.html -- Service status: https://chat.ecnu.edu.cn/status +| 2026-08-10 | Reasoning-effort controls and compatibility mappings | +| 2026-08-09 | Security and developer-agreement documentation update | +| 2026-08-03 | TTS upgraded to Fun-CosyVoice3-0.5B; additional voices | +| 2026-08-01 | `ecnu-max` upgraded to DeepSeek-V4-Flash-0731 | +| 2026-04-24 | `ecnu-max` vision removal announced | +| 2026-04-03 | Dialog models consolidated to `ecnu-max` and `ecnu-plus` | +| 2025-03-20 | Native `search_mode` removed | + +## Official sources + +- https://developer.ecnu.edu.cn/vitepress/llm/model.html +- https://developer.ecnu.edu.cn/vitepress/llm/thinking.html +- https://developer.ecnu.edu.cn/vitepress/llm/limit.html +- https://developer.ecnu.edu.cn/vitepress/llm/release.html +- https://developer.ecnu.edu.cn/vitepress/llm/security.html +- https://developer.ecnu.edu.cn/vitepress/llm/tos.html +- https://chat.ecnu.edu.cn/status diff --git a/references/workflows.md b/references/workflows.md new file mode 100644 index 0000000..2817555 --- /dev/null +++ b/references/workflows.md @@ -0,0 +1,202 @@ +# Agent Workflows + +Use these procedures when implementing, reviewing, troubleshooting, or live +testing ECNU API integrations. + +## Implementation workflow + +1. Identify the requested capability and protocol. +2. Read the relevant endpoint contract. +3. Select a primary model rather than a historical alias. +4. Build the smallest valid request. +5. Keep the API key in an environment variable. +6. Add explicit timeouts. +7. Validate the HTTP status, content type, and response shape. +8. Add advanced fields one at a time. +9. Keep calls sequential unless ECNU documents otherwise. +10. Report which behavior is documented and which is application policy. + +Do not start from a generic OpenAI example and assume every field is supported. + +## Code-review checklist + +Check: + +- correct base URL and endpoint; +- correct model for the capability; +- environment-based credential loading; +- no key, ticket, or embedded URL in logs; +- documented JSON types; +- no OpenAI token-ID input for ECNU embeddings; +- no undocumented embedding dimension request; +- `ecnu-plus` for image understanding; +- explicit timeout; +- bounded error-body capture; +- no automatic parallel batch; +- no blind retry for billable POST requests; +- output validation; +- privacy and data-minimization requirements. + +## Troubleshooting workflow + +### 1. Capture evidence + +Collect: + +- UTC timestamp; +- endpoint and method; +- model name; +- HTTP status; +- response content type; +- bounded, redacted body sample; +- request field names and JSON types; +- SDK and version, if applicable; +- whether the request was direct HTTP or an SDK call. + +Do not collect the API key or full sensitive prompt. + +### 2. Classify the failure + +| Symptom | First checks | +|---|---| +| `401` | credential source, expiry, protocol-specific auth handling | +| `403` | application or IP allowlist | +| `422` | missing field, wrong JSON type, unsupported request shape | +| `429` | credits, concurrent requests, burst protection | +| `5xx` JSON | proxy or backend error details | +| `5xx` plain text/HTML | preserve content type and bounded body | +| `200` with empty data | do not assume success; validate semantics | +| timeout | determine whether the server may still have accepted the request | + +### 3. Retry safely + +Safe read-style requests may use limited exponential backoff with jitter. + +For chat, embedding, and rerank, retry only when the application can tolerate +duplicate work and the error is clearly transient. + +For image generation, TTS, or any billed operation, do not automatically +resubmit after a timeout or connection drop unless the service provides an +idempotency mechanism or the user explicitly accepts duplicate charges. + +Never retry: + +- unchanged invalid credentials; +- a deterministic `422`; +- a rejected request shape; +- a known unsupported model. + +### 4. Compare sources + +Use this order: + +1. endpoint documentation; +2. model and quota pages; +3. dated known deviations; +4. a controlled live probe. + +Do not use a single `/models` result as proof of endpoint support. + +## Live-verification workflow + +Use `scripts/smoke_test.py`. Its default profile limits itself to model-list +requests. + +Before opt-in POST probes: + +1. confirm the account owner authorized testing; +2. confirm expected credit use; +3. minimize prompts and output tokens; +4. remove personal or confidential data; +5. set an explicit timeout; +6. avoid parallel execution; +7. write only a sanitized structural report. + +Example: + +```bash +export ECNU_API_KEY="your-api-key" +python scripts/smoke_test.py --low-cost --anthropic \ + --account-type personal-token \ + --output smoke-results.json +``` + +A valid report should record: + +- test date and Python version; +- enabled profiles; +- status and content type per request; +- model IDs for `/models`; +- vector count and output length for embeddings; +- response structure, not successful model text; +- transport errors without credentials. + +If the environment cannot reach the ECNU host, label the behavior unverified. +Do not update the known-deviation date. + +## Model-discovery workflow + +1. Read the model page for supported primary models and capabilities. +2. Call `/models` for runtime visibility. +3. Reject an empty list as inconclusive rather than authenticated success. +4. Ignore undocumented model IDs for production selection until a controlled + capability probe succeeds. +5. Record capability probes by endpoint, because one model ID may not work + across every endpoint. +6. Prefer documented primary names even when aliases are visible. + +## Embedding workflow + +1. Validate `input` as `str` or non-empty `list[str]`. +2. Reject integer arrays. +3. Keep batches conservative and sequential. +4. With LangChain, disable client-side OpenAI token conversion. +5. Do not send an undocumented dimension-selection field. +6. Sort returned items by `index`. +7. Assert that each returned vector has 1024 values. +8. Split or reduce a batch only after a meaningful validation or size error; + do not claim the resulting size is an ECNU maximum. + +## Anthropic workflow + +1. Set `ANTHROPIC_BASE_URL` to the Anthropic root, not the OpenAI root. +2. Use `ANTHROPIC_AUTH_TOKEN` from the environment. +3. Prefer `ecnu-plus` or plain `ecnu-max`. +4. Use `ecnu-max[1m]` only when a tool requires the suffix to advertise long + context. +5. On suffix-specific metadata or authentication failure, fall back to plain + `ecnu-max` and report the capability difference. +6. Do not generalize the suffix to OpenAI-compatible APIs. + +## Security and privacy workflow + +- Do not request a key in chat when an environment variable or secret manager + can be used. +- If a key was exposed, recommend rotation. +- Do not send private documents, images, secrets, personal information, or + internal prompts without a clear user request and appropriate handling basis. +- Do not log embed tickets, one-time URLs, authorization headers, or raw + production prompts. +- Sanitize exception bodies because proxies may echo request details. +- Keep live-test artifacts out of version control. + +## Result format + +A useful report distinguishes: + +```text +Documented: +- ... + +Observed on YYYY-MM-DD: +- ... + +Unverified or environment-limited: +- ... + +Recommended application policy: +- ... +``` + +This prevents application safeguards and one-time observations from being +mistaken for platform guarantees. diff --git a/scripts/smoke_test.py b/scripts/smoke_test.py new file mode 100755 index 0000000..e3e99c2 --- /dev/null +++ b/scripts/smoke_test.py @@ -0,0 +1,452 @@ +#!/usr/bin/env python3 +"""Sanitized structural smoke tests for the ECNU API. + +The default profile performs model-list checks only. POST probes that may +consume credits are opt-in. The script never prints successful model content +or the API key. +""" + +from __future__ import annotations + +import argparse +import json +import os +import platform +import re +import sys +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Iterable, Mapping +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen + +DEFAULT_OPENAI_BASE = "https://chat.ecnu.edu.cn/open/api/v1" +DEFAULT_ANTHROPIC_BASE = "https://chat.ecnu.edu.cn/open/api/anthropic" +KEY_PATTERN = re.compile(r"\bsk-[A-Za-z0-9_-]{8,}\b") +MAX_ERROR_TEXT = 1000 + + +@dataclass(frozen=True) +class HttpResult: + status: int | None + headers: dict[str, str] + body: bytes + transport_error: str | None = None + + +def redact_text(text: str, secrets: Iterable[str] = ()) -> str: + """Redact exact secrets and key-shaped strings.""" + redacted = text + for secret in secrets: + if secret: + redacted = redacted.replace(secret, "[REDACTED_API_KEY]") + return KEY_PATTERN.sub("[REDACTED_API_KEY]", redacted) + + +def sanitize_value(value: Any, secrets: Iterable[str] = ()) -> Any: + """Bound and redact a JSON-compatible value for a report.""" + if isinstance(value, str): + return redact_text(value, secrets)[:MAX_ERROR_TEXT] + if isinstance(value, list): + return [sanitize_value(item, secrets) for item in value[:20]] + if isinstance(value, dict): + result: dict[str, Any] = {} + for index, (key, item) in enumerate(value.items()): + if index >= 30: + result["..."] = "truncated" + break + lowered = str(key).lower() + if lowered in {"authorization", "x-api-key", "api_key", "token"}: + result[str(key)] = "[REDACTED]" + else: + result[str(key)] = sanitize_value(item, secrets) + return result + return value + + +def build_embedding_payload(input_value: str | list[str]) -> dict[str, Any]: + """Build only the documented ECNU embedding request fields.""" + if isinstance(input_value, str): + if not input_value: + raise ValueError("embedding input must not be empty") + elif isinstance(input_value, list): + if not input_value or not all( + isinstance(item, str) and item for item in input_value + ): + raise TypeError("embedding input must be a non-empty string array") + else: + raise TypeError("embedding input must be a string or string array") + + return { + "model": "ecnu-embedding-small", + "input": input_value, + } + + +def request( + method: str, + url: str, + *, + headers: Mapping[str, str] | None = None, + payload: Mapping[str, Any] | None = None, + timeout: float = 30.0, +) -> HttpResult: + data = None + request_headers = dict(headers or {}) + if payload is not None: + data = json.dumps(payload, ensure_ascii=False).encode("utf-8") + request_headers.setdefault("Content-Type", "application/json") + + req = Request( + url=url, + data=data, + headers=request_headers, + method=method, + ) + + try: + with urlopen(req, timeout=timeout) as response: + return HttpResult( + status=response.status, + headers={key.lower(): value for key, value in response.headers.items()}, + body=response.read(), + ) + except HTTPError as exc: + return HttpResult( + status=exc.code, + headers={key.lower(): value for key, value in exc.headers.items()}, + body=exc.read(), + ) + except (URLError, TimeoutError, OSError) as exc: + return HttpResult( + status=None, + headers={}, + body=b"", + transport_error=f"{type(exc).__name__}: {exc}", + ) + + +def parse_json(body: bytes) -> Any | None: + try: + return json.loads(body.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError): + return None + + +def summarize_response( + name: str, + result: HttpResult, + *, + secrets: Iterable[str] = (), +) -> dict[str, Any]: + summary: dict[str, Any] = { + "status": result.status, + "content_type": result.headers.get("content-type", ""), + "body_bytes": len(result.body), + } + + if result.transport_error: + summary["transport_error"] = redact_text( + result.transport_error, + secrets, + )[:MAX_ERROR_TEXT] + return summary + + payload = parse_json(result.body) + + if name.startswith("models_") and isinstance(payload, dict): + data = payload.get("data") + summary["object"] = payload.get("object") + summary["model_ids"] = [ + item.get("id") + for item in data or [] + if isinstance(item, dict) and isinstance(item.get("id"), str) + ][:100] + + elif name.startswith("chat_") and isinstance(payload, dict): + summary["has_choices"] = bool(payload.get("choices")) + summary["model"] = payload.get("model") + usage = payload.get("usage") + summary["usage_keys"] = sorted(usage.keys()) if isinstance(usage, dict) else [] + + elif name.startswith("embedding_") and isinstance(payload, dict): + data = payload.get("data") + items = data if isinstance(data, list) else [] + summary["count"] = len(items) + summary["dimensions"] = [ + len(item.get("embedding", [])) + for item in items + if isinstance(item, dict) and isinstance(item.get("embedding"), list) + ] + + elif name.startswith("anthropic_") and isinstance(payload, dict): + content = payload.get("content") + summary["type"] = payload.get("type") + summary["model"] = payload.get("model") + summary["content_types"] = [ + item.get("type") + for item in content or [] + if isinstance(item, dict) + ] + + if result.status is None or result.status >= 400: + if payload is not None: + summary["error"] = sanitize_value(payload, secrets) + else: + text = result.body.decode("utf-8", errors="replace") + summary["error_text"] = redact_text(text, secrets)[:MAX_ERROR_TEXT] + + return summary + + +def run_case( + name: str, + method: str, + url: str, + *, + headers: Mapping[str, str] | None, + payload: Mapping[str, Any] | None, + timeout: float, + secret: str, +) -> dict[str, Any]: + result = request( + method, + url, + headers=headers, + payload=payload, + timeout=timeout, + ) + return summarize_response(name, result, secrets=(secret,)) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Run sanitized structural ECNU API smoke tests." + ) + parser.add_argument( + "--low-cost", + action="store_true", + help="Add small Chat Completions and embedding POST probes.", + ) + parser.add_argument( + "--anthropic", + action="store_true", + help=( + "Add small Anthropic probes for ecnu-max and ecnu-max[1m]; " + "these requests may consume credits." + ), + ) + parser.add_argument( + "--timeout", + type=float, + default=30.0, + help="Per-request timeout in seconds (default: 30).", + ) + parser.add_argument( + "--openai-base", + default=DEFAULT_OPENAI_BASE, + help="OpenAI-compatible base URL.", + ) + parser.add_argument( + "--anthropic-base", + default=DEFAULT_ANTHROPIC_BASE, + help="Anthropic-compatible base URL.", + ) + parser.add_argument( + "--account-type", + default="unspecified", + help="Non-secret account label for the report, such as personal-token.", + ) + parser.add_argument( + "--output", + type=Path, + help="Optional JSON report path; stdout is always printed.", + ) + parser.add_argument( + "--strict", + action="store_true", + help="Return non-zero when required enabled checks fail.", + ) + return parser + + +def required_check_failed( + name: str, + summary: Mapping[str, Any], + *, + enabled_low_cost: bool, + enabled_anthropic: bool, +) -> bool: + status = summary.get("status") + if name == "models_valid": + return status != 200 or not summary.get("model_ids") + if enabled_low_cost and name == "chat_plus": + return status != 200 or not summary.get("has_choices") + if enabled_low_cost and name.startswith("embedding_"): + return status != 200 or not summary.get("dimensions") + if enabled_anthropic and name == "anthropic_ecnu_max": + return status != 200 + # Invalid/missing-auth behavior and the [1m] suffix are diagnostic probes. + return False + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + api_key = os.environ.get("ECNU_API_KEY") + if not api_key: + print( + "ECNU_API_KEY is required. Store it in the environment; " + "do not pass it as a command-line argument.", + file=sys.stderr, + ) + return 2 + + openai_base = args.openai_base.rstrip("/") + anthropic_messages = args.anthropic_base.rstrip("/") + "/v1/messages" + auth_headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + + report: dict[str, Any] = { + "meta": { + "tested_at_utc": datetime.now(timezone.utc).isoformat(), + "python": platform.python_version(), + "account_type": args.account_type, + "openai_base": openai_base, + "anthropic_base": args.anthropic_base.rstrip("/"), + "profiles": { + "models": True, + "low_cost": args.low_cost, + "anthropic": args.anthropic, + }, + "notes": [ + "Successful model output is intentionally omitted.", + "Image generation and TTS are intentionally not automated.", + "Statuses are observations, not permanent API contracts.", + ], + }, + "tests": {}, + } + tests: dict[str, Any] = report["tests"] + + tests["models_valid"] = run_case( + "models_valid", + "GET", + openai_base + "/models", + headers={"Authorization": f"Bearer {api_key}"}, + payload=None, + timeout=args.timeout, + secret=api_key, + ) + tests["models_invalid"] = run_case( + "models_invalid", + "GET", + openai_base + "/models", + headers={"Authorization": "Bearer invalid-smoke-test-token"}, + payload=None, + timeout=args.timeout, + secret=api_key, + ) + tests["models_missing"] = run_case( + "models_missing", + "GET", + openai_base + "/models", + headers=None, + payload=None, + timeout=args.timeout, + secret=api_key, + ) + + if args.low_cost: + tests["chat_plus"] = run_case( + "chat_plus", + "POST", + openai_base + "/chat/completions", + headers=auth_headers, + payload={ + "model": "ecnu-plus", + "messages": [ + {"role": "user", "content": "Reply with exactly: ok"} + ], + "max_tokens": 8, + }, + timeout=args.timeout, + secret=api_key, + ) + tests["embedding_scalar"] = run_case( + "embedding_scalar", + "POST", + openai_base + "/embeddings", + headers=auth_headers, + payload=build_embedding_payload("smoke test"), + timeout=args.timeout, + secret=api_key, + ) + tests["embedding_array"] = run_case( + "embedding_array", + "POST", + openai_base + "/embeddings", + headers=auth_headers, + payload=build_embedding_payload(["one", "two"]), + timeout=args.timeout, + secret=api_key, + ) + + if args.anthropic: + anthropic_headers = { + "Authorization": f"Bearer {api_key}", + "x-api-key": api_key, + "anthropic-version": "2023-06-01", + "Content-Type": "application/json", + } + for model, name in ( + ("ecnu-max", "anthropic_ecnu_max"), + ("ecnu-max[1m]", "anthropic_ecnu_max_1m"), + ): + tests[name] = run_case( + name, + "POST", + anthropic_messages, + headers=anthropic_headers, + payload={ + "model": model, + "max_tokens": 8, + "messages": [ + {"role": "user", "content": "Reply with exactly: ok"} + ], + }, + timeout=args.timeout, + secret=api_key, + ) + + output_text = json.dumps(report, ensure_ascii=False, indent=2) + print(output_text) + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(output_text + "\n", encoding="utf-8") + + if args.strict: + failures = [ + name + for name, summary in tests.items() + if required_check_failed( + name, + summary, + enabled_low_cost=args.low_cost, + enabled_anthropic=args.anthropic, + ) + ] + if failures: + print( + "Strict checks failed: " + ", ".join(failures), + file=sys.stderr, + ) + return 1 + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validate_skill.py b/scripts/validate_skill.py new file mode 100755 index 0000000..9ff8c71 --- /dev/null +++ b/scripts/validate_skill.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Offline validation for the ecnu-api Agent Skill repository.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SKILL = ROOT / "SKILL.md" + +REQUIRED_FILES = [ + "SKILL.md", + "README.md", + "AGENTS.md", + "references/api_reference.md", + "references/models.md", + "references/examples.md", + "references/workflows.md", + "references/known_deviations.md", + "scripts/smoke_test.py", + "scripts/validate_skill.py", + "tests/test_smoke_test.py", +] + +NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") +LINK_RE = re.compile(r"\[[^\]]+\]\(([^)]+)\)") +SECRET_RE = re.compile(r"\bsk-[A-Za-z0-9]{24,}\b") +DIMENSION_ASSIGNMENT_RE = re.compile(r"\bdimensions\s*=\s*1024\b") +WINDOWS_USER_PATH_RE = re.compile(r"[A-Za-z]:\\Users\\") + + +def parse_frontmatter(text: str) -> tuple[dict[str, str], str]: + if not text.startswith("---\n"): + raise ValueError("SKILL.md must start with YAML frontmatter") + try: + _, raw_frontmatter, body = text.split("---\n", 2) + except ValueError as exc: + raise ValueError("SKILL.md frontmatter is not closed") from exc + + fields: dict[str, str] = {} + lines = raw_frontmatter.splitlines() + index = 0 + while index < len(lines): + line = lines[index] + if not line.strip(): + index += 1 + continue + if ":" not in line: + raise ValueError(f"invalid frontmatter line: {line!r}") + key, value = line.split(":", 1) + key = key.strip() + value = value.strip() + if value in {">", "|"}: + continuation: list[str] = [] + index += 1 + while index < len(lines): + candidate = lines[index] + if candidate and not candidate.startswith((" ", "\t")): + break + continuation.append(candidate.strip()) + index += 1 + fields[key] = " ".join(part for part in continuation if part) + continue + fields[key] = value.strip("\"'") + index += 1 + return fields, body + + +def iter_text_files() -> list[Path]: + paths: list[Path] = [] + for path in ROOT.rglob("*"): + if not path.is_file(): + continue + if any(part in {".git", ".venv", "__pycache__"} for part in path.parts): + continue + if path.suffix.lower() in { + ".md", + ".py", + ".yml", + ".yaml", + ".txt", + ".gitignore", + } or path.name == ".gitignore": + paths.append(path) + return paths + + +def main() -> int: + errors: list[str] = [] + + for relative in REQUIRED_FILES: + if not (ROOT / relative).is_file(): + errors.append(f"missing required file: {relative}") + + if not SKILL.is_file(): + for error in errors: + print(f"ERROR: {error}") + return 1 + + text = SKILL.read_text(encoding="utf-8") + try: + fields, body = parse_frontmatter(text) + except ValueError as exc: + errors.append(str(exc)) + fields, body = {}, "" + + name = fields.get("name", "") + description = fields.get("description", "") + + if not name: + errors.append("frontmatter name is required") + elif len(name) > 64 or not NAME_RE.fullmatch(name): + errors.append("frontmatter name must be lowercase kebab-case and <=64 chars") + elif ROOT.name != name: + # Allow a checkout directory suffix in temporary or CI worktrees. + if not ROOT.name.startswith(name): + errors.append( + f"skill directory {ROOT.name!r} does not match name {name!r}" + ) + + if not description: + errors.append("frontmatter description is required") + elif len(description) > 1024: + errors.append("frontmatter description exceeds 1024 characters") + + if len(body.splitlines()) > 500: + errors.append("SKILL.md body exceeds the recommended 500 lines") + + for target in LINK_RE.findall(body): + if "://" in target or target.startswith("#"): + continue + file_target = target.split("#", 1)[0] + if file_target and not (ROOT / file_target).exists(): + errors.append(f"broken SKILL.md link: {target}") + + for path in iter_text_files(): + content = path.read_text(encoding="utf-8", errors="replace") + relative = path.relative_to(ROOT) + if SECRET_RE.search(content): + errors.append(f"possible committed API key in {relative}") + if DIMENSION_ASSIGNMENT_RE.search(content): + errors.append( + f"undocumented LangChain dimension request in {relative}" + ) + if WINDOWS_USER_PATH_RE.search(content): + errors.append(f"machine-specific Windows user path in {relative}") + + if errors: + for error in errors: + print(f"ERROR: {error}") + return 1 + + print("Skill validation passed.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_smoke_test.py b/tests/test_smoke_test.py new file mode 100644 index 0000000..0af9002 --- /dev/null +++ b/tests/test_smoke_test.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) + +import smoke_test # noqa: E402 + + +class SmokeTestHelpersTest(unittest.TestCase): + def test_redact_text_removes_exact_and_key_shaped_values(self) -> None: + secret = "test-secret-value" + key_shaped = "sk-" + ("a" * 32) + text = ( + "Authorization: Bearer test-secret-value " + f"and {key_shaped}" + ) + redacted = smoke_test.redact_text(text, (secret,)) + self.assertNotIn(secret, redacted) + self.assertNotIn(key_shaped, redacted) + self.assertGreaterEqual(redacted.count("[REDACTED_API_KEY]"), 2) + + def test_embedding_payload_uses_only_documented_fields(self) -> None: + payload = smoke_test.build_embedding_payload(["one", "two"]) + self.assertEqual( + payload, + { + "model": "ecnu-embedding-small", + "input": ["one", "two"], + }, + ) + + def test_embedding_payload_rejects_token_ids(self) -> None: + with self.assertRaises(TypeError): + smoke_test.build_embedding_payload([1, 2]) # type: ignore[arg-type] + + def test_models_summary_keeps_structure_not_raw_body(self) -> None: + result = smoke_test.HttpResult( + status=200, + headers={"content-type": "application/json"}, + body=( + b'{"object":"list","data":[' + b'{"id":"ecnu-plus"},{"id":"ecnu-max"}]}' + ), + ) + summary = smoke_test.summarize_response("models_valid", result) + self.assertEqual(summary["model_ids"], ["ecnu-plus", "ecnu-max"]) + self.assertNotIn("body", summary) + + def test_error_summary_redacts_and_bounds_text(self) -> None: + secret = "test-secret-value" + result = smoke_test.HttpResult( + status=500, + headers={"content-type": "text/plain"}, + body=(secret + " " + ("x" * 5000)).encode(), + ) + summary = smoke_test.summarize_response( + "other", + result, + secrets=(secret,), + ) + error_text = summary["error_text"] + self.assertNotIn(secret, error_text) + self.assertLessEqual(len(error_text), smoke_test.MAX_ERROR_TEXT) + + +if __name__ == "__main__": + unittest.main()