KEATH.AIKEATH.AI
  • Home
  • KEATH Public API v1
  • Examples and Test Snippets
  • Migration from the Legacy API
  • 主页
  • KEATH Public API v1
  • 示例与测试代码
  • 从旧接口迁移
  • Accueil
  • API publique KEATH v1
  • Exemples et tests rapides
  • Migration depuis l'ancienne API
  • Inicio
  • API pública KEATH v1
  • Ejemplos y pruebas
  • Migración desde la API anterior
  • الصفحة الرئيسية
  • KEATH Public API v1
  • أمثلة واختبارات سريعة
  • الانتقال من الواجهة القديمة
  • Home
  • KEATH Public API v1
  • Examples and Test Snippets
  • Migration from the Legacy API
  • 主页
  • KEATH Public API v1
  • 示例与测试代码
  • 从旧接口迁移
  • Accueil
  • API publique KEATH v1
  • Exemples et tests rapides
  • Migration depuis l'ancienne API
  • Inicio
  • API pública KEATH v1
  • Ejemplos y pruebas
  • Migración desde la API anterior
  • الصفحة الرئيسية
  • KEATH Public API v1
  • أمثلة واختبارات سريعة
  • الانتقال من الواجهة القديمة
  • KEATH Public API v1
  • 示例与测试代码
  • 从旧接口迁移

示例与测试代码

快速联调顺序

建议按这个顺序做 smoke test:

  1. 调 GET /credits
  2. 调 GET /models
  3. 用 POST /assignments 创建学生 assignment;如果不想创建 assignment,就走 POST /evaluations/one-pass
  4. 用返回的 assignment_id 调 POST /evaluations
  5. 再轮询 GET /evaluations/:taskId/status
  6. 再拿最终结果 GET /evaluations/:taskId
  7. 再调 POST /feedback-rewrite
  8. 如果你还需要新建 marking model,再测试 questions-ingest-preview、rubrics-ingest-preview 和 POST /models

如果前两步正常,但 evaluation 失败,最常见的原因是把 model_id 当成了 assignment_id、所选 model 还没 ready,或者客户端还停留在旧的 X-TOKEN-KEY 流程。

这些示例里的 API key 是以 kct_ 开头的 KEATH Public API key。Gemini、NewAPI 这类模型供应商 key 只应该留在 KEATH 服务端,不应该从你的客户端传给 Public API。

推荐测试文本

当源文件里有多个题目、封面页或 sample answer 时,可以先用这些 specification 测试。

题目解析:

只解析 Question 2。忽略封面页、sample answer 和 teacher notes。如果 PDF 里有多个任务,只聚焦 situational writing 这一题。

Rubric 解析:

只使用目标写作任务对应的 rubric 表格。忽略 sample answer、解释性说明和其他 worksheet。

Feedback Rewrite:

把 feedback 改得更短、更清晰、更像老师直接给学生的反馈,但不要改变原有判断。

推荐测试作文文本

如果你想验证 POST /evaluations,又不想直接用真实学生作文,可以先用这一段安全样例:

Dear Mrs Tan,

I am writing to request permission for our class to organise a recycling drive next Friday after school. Many students have noticed that large amounts of paper bottles and food packaging are thrown away every day. We believe a short class project would help students build better habits and understand why recycling matters.

If the school approves the activity our class can prepare labelled collection boxes and take turns to explain the instructions to other students. We can also make a short announcement during assembly so that everyone knows what items can be collected.

I hope you will consider this proposal. Thank you for your time and support.

Yours sincerely,
Jamie Lee

curl 示例

查询 credits

curl "https://keath.ai/api/keath/public/v1/credits" \
  -H "X-API-Key: kct_your_api_key"

直接上传 PDF 解析题目

curl -X POST "https://keath.ai/api/keath/public/v1/questions-ingest-preview" \
  -H "X-API-Key: kct_your_api_key" \
  -F "file=@./question-pack.pdf;type=application/pdf" \
  -F "specification=只解析 Question 2,忽略 sample answer 页面。"

如果你的 HTTP 客户端不能设置 type=application/pdf,KEATH 会尝试根据文件扩展名兜底识别常见 PDF / 图片类型。仍然建议显式传 MIME type,这样更清晰,也更不容易被代理或 SDK 改坏。

上传图片并混合已有 URL

curl -X POST "https://keath.ai/api/keath/public/v1/questions-ingest-preview" \
  -H "X-API-Key: kct_your_api_key" \
  -F "files=@./scan-1.jpg;type=image/jpeg" \
  -F 'assets=[{"url":"https://cdn.example.com/supporting-page.pdf","mime_type":"application/pdf","name":"supporting-page.pdf"}]' \
  -F "specification=以上传图片为目标题目。"

直接上传 rubric

curl -X POST "https://keath.ai/api/keath/public/v1/rubrics-ingest-preview" \
  -H "X-API-Key: kct_your_api_key" \
  -F "file=@./rubric.pdf;type=application/pdf" \
  -F "total_score=30" \
  -F "cum_method=sum" \
  -F "specification=只用第一页的 rubric 表格。"

用 model 创建 assignment

curl -X POST "https://keath.ai/api/keath/public/v1/assignments" \
  -H "X-API-Key: kct_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "model_id": 930,
    "assignment_name": "Situation Writing Test",
    "assignment_desc": "Student-facing writing assignment",
    "project_subject": "English",
    "deadline_time": "2026-06-01T00:00:00.000Z",
    "expected_number": 30,
    "file_type": "pdf"
  }'

提交一次评测

curl -X POST "https://keath.ai/api/keath/public/v1/evaluations" \
  -H "X-API-Key: kct_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "assignment_id": 1620,
    "paper_content": "Dear Mrs Tan,\n\nI am writing to request permission for our class to organise a recycling drive next Friday after school...",
    "style": "Bullet"
  }'

上传学生作答文件提交评测

curl -X POST "https://keath.ai/api/keath/public/v1/evaluations" \
  -H "X-API-Key: kct_your_api_key" \
  -F "assignment_id=1620" \
  -F "answer_file=@./student-answer.pdf;type=application/pdf" \
  -F "student_id=anon-student-001" \
  -F "style=Bullet"

One-pass 一次性评测

curl -X POST "https://keath.ai/api/keath/public/v1/evaluations/one-pass" \
  -H "X-API-Key: kct_your_api_key" \
  -H "Idempotency-Key: eval-student-001-attempt-1" \
  -F "model_id=930" \
  -F "question_file=@./question.pdf;type=application/pdf" \
  -F "rubric_file=@./rubric.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document" \
  -F "answer_file=@./student-answer.txt;type=text/plain" \
  -F "student_id=anon-student-001" \
  -F "specification=只使用 Question 2。" \
  -F "style=Bullet"

接口返回 HTTP 202。请保存公开 task_id,不要在这次请求里等待评分结果。

批量 one-pass 评测

curl -X POST "https://keath.ai/api/keath/public/v1/evaluations/batch" \
  -H "X-API-Key: kct_your_api_key" \
  -H "Idempotency-Key: class-5a-writing-2026-07-15" \
  -H "Content-Type: application/json" \
  -d '{
    "evaluations": [
      {"model_id":930,"question_text":"写一封正式信件。","paper_content":"尊敬的校长:……","student_id":"anon-001"},
      {"model_id":930,"question_text":"写一封正式信件。","paper_content":"尊敬的校长,我建议……","student_id":"anon-002"}
    ],
    "callback_url":"https://integration.example.com/keath/results"
  }'

轮询评测状态

curl "https://keath.ai/api/keath/public/v1/evaluations/task_123/status" \
  -H "X-API-Key: kct_your_api_key"

获取评测结果

curl "https://keath.ai/api/keath/public/v1/evaluations/task_123" \
  -H "X-API-Key: kct_your_api_key"

取消评测

curl -X POST "https://keath.ai/api/keath/public/v1/evaluations/task_123/cancel" \
  -H "X-API-Key: kct_your_api_key"

Feedback Rewrite

curl -X POST "https://keath.ai/api/keath/public/v1/feedback-rewrite" \
  -H "X-API-Key: kct_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "style": "bullet_points",
    "instruction": "把内容写得更短一点,更像老师给学生的反馈。",
    "sections": [
      {
        "item": "Content",
        "comment": "Your answer contains relevant ideas but the explanation remains underdeveloped.",
        "score": 7,
        "max_score": 10
      }
    ]
  }'

JavaScript 示例

const apiKey = process.env.KEATH_API_KEY
const baseUrl = 'https://keath.ai/api/keath/public/v1'

async function getCredits() {
  const response = await fetch(`${baseUrl}/credits`, {
    headers: {
      'X-API-Key': apiKey,
    },
  })
  return response.json()
}

async function previewQuestionsWithPdf(file) {
  const formData = new FormData()
  formData.append('file', file, file.name)
  formData.append('specification', '只解析 Question 2,忽略 sample answer 页面。')

  const response = await fetch(`${baseUrl}/questions-ingest-preview`, {
    method: 'POST',
    headers: {
      'X-API-Key': apiKey,
    },
    body: formData,
  })

  return response.json()
}

async function createAssignment(modelId) {
  const response = await fetch(`${baseUrl}/assignments`, {
    method: 'POST',
    headers: {
      'X-API-Key': apiKey,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model_id: modelId,
      assignment_name: 'Situation Writing Test',
      assignment_desc: 'Student-facing writing assignment',
      project_subject: 'English',
      deadline_time: '2026-06-01T00:00:00.000Z',
      expected_number: 30,
      file_type: 'pdf',
    }),
  })

  return response.json()
}

async function submitEvaluation(assignmentId, paperContent) {
  const response = await fetch(`${baseUrl}/evaluations`, {
    method: 'POST',
    headers: {
      'X-API-Key': apiKey,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      assignment_id: assignmentId,
      paper_content: paperContent,
      style: 'Bullet',
    }),
  })

  return response.json()
}

async function submitEvaluationWithFile(assignmentId, answerFile) {
  const formData = new FormData()
  formData.append('assignment_id', String(assignmentId))
  formData.append('answer_file', answerFile, answerFile.name)
  formData.append('student_id', 'anon-student-001')
  formData.append('style', 'Bullet')

  const response = await fetch(`${baseUrl}/evaluations`, {
    method: 'POST',
    headers: {
      'X-API-Key': apiKey,
    },
    body: formData,
  })

  return response.json()
}

async function submitOnePassEvaluation({ modelId, questionFile, rubricFile, answerFile, requestId }) {
  const formData = new FormData()
  formData.append('model_id', String(modelId))
  formData.append('question_file', questionFile, questionFile.name)
  formData.append('rubric_file', rubricFile, rubricFile.name)
  formData.append('answer_file', answerFile, answerFile.name)
  formData.append('student_id', 'anon-student-001')
  formData.append('specification', '只使用 Question 2。')
  formData.append('style', 'Bullet')

  const response = await fetch(`${baseUrl}/evaluations/one-pass`, {
    method: 'POST',
    headers: {
      'X-API-Key': apiKey,
      'Idempotency-Key': requestId,
    },
    body: formData,
  })

  return response.json()
}

async function pollEvaluationUntilDone(taskId, options = {}) {
  const maxAttempts = options.maxAttempts ?? 20
  const initialDelayMs = options.initialDelayMs ?? 3000
  const maxDelayMs = options.maxDelayMs ?? 15000

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const response = await fetch(`${baseUrl}/evaluations/${taskId}/status`, {
      headers: {
        'X-API-Key': apiKey,
      },
    })
    const payload = await response.json()
    const status = payload.data?.status || payload.status

    if (status === 'SUCCESS' || status === 'FAILURE' || status === 'CANCEL') {
      return payload
    }

    const delayMs = Math.min(initialDelayMs * attempt, maxDelayMs)
    await new Promise(resolve => setTimeout(resolve, delayMs))
  }

  throw new Error(`评测任务 ${taskId} 在 ${maxAttempts} 次轮询后仍未结束`)
}

Python 示例

import json
import time
import requests

API_KEY = "kct_your_api_key"
BASE_URL = "https://keath.ai/api/keath/public/v1"


def get_credits():
    response = requests.get(
        f"{BASE_URL}/credits",
        headers={"X-API-Key": API_KEY},
        timeout=60,
    )
    response.raise_for_status()
    return response.json()


def preview_questions_with_pdf(path: str):
    with open(path, "rb") as f:
        response = requests.post(
            f"{BASE_URL}/questions-ingest-preview",
            headers={"X-API-Key": API_KEY},
            files={"file": ("question-pack.pdf", f, "application/pdf")},
            data={"specification": "只解析 Question 2,忽略 sample answer 页面。"},
            timeout=300,
        )
    response.raise_for_status()
    return response.json()


def preview_questions_mixed(path: str):
    with open(path, "rb") as f:
        response = requests.post(
            f"{BASE_URL}/questions-ingest-preview",
            headers={"X-API-Key": API_KEY},
            files={"file": ("scan-1.jpg", f, "image/jpeg")},
            data={
                "assets": json.dumps(
                    [
                        {
                            "url": "https://cdn.example.com/supporting-page.pdf",
                            "mime_type": "application/pdf",
                            "name": "supporting-page.pdf",
                        }
                    ]
                ),
                "specification": "以上传图片为目标题目。",
            },
            timeout=300,
        )
    response.raise_for_status()
    return response.json()


def submit_evaluation(assignment_id: int, paper_content: str):
    response = requests.post(
        f"{BASE_URL}/evaluations",
        headers={
            "X-API-Key": API_KEY,
            "Content-Type": "application/json",
        },
        json={
            "assignment_id": assignment_id,
            "paper_content": paper_content,
            "style": "Bullet",
        },
        timeout=300,
    )
    response.raise_for_status()
    return response.json()


def submit_evaluation_with_file(assignment_id: int, answer_path: str):
    with open(answer_path, "rb") as f:
        response = requests.post(
            f"{BASE_URL}/evaluations",
            headers={"X-API-Key": API_KEY},
            data={
                "assignment_id": str(assignment_id),
                "student_id": "anon-student-001",
                "style": "Bullet",
            },
            files={"answer_file": ("student-answer.pdf", f, "application/pdf")},
            timeout=300,
        )
    response.raise_for_status()
    return response.json()


def submit_one_pass_evaluation(
    model_id: int,
    question_path: str,
    rubric_path: str,
    answer_path: str,
    request_id: str,
):
    with open(question_path, "rb") as question, open(rubric_path, "rb") as rubric, open(answer_path, "rb") as answer:
        response = requests.post(
            f"{BASE_URL}/evaluations/one-pass",
            headers={
                "X-API-Key": API_KEY,
                "Idempotency-Key": request_id,
            },
            data={
                "model_id": str(model_id),
                "student_id": "anon-student-001",
                "specification": "只使用 Question 2。",
                "style": "Bullet",
            },
            files={
                "question_file": ("question.pdf", question, "application/pdf"),
                "rubric_file": (
                    "rubric.docx",
                    rubric,
                    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                ),
                "answer_file": ("student-answer.txt", answer, "text/plain"),
            },
            timeout=300,
        )
    response.raise_for_status()
    return response.json()


def wait_for_evaluation(task_id: str):
    max_attempts = 20
    initial_delay_seconds = 3
    max_delay_seconds = 15

    for attempt in range(1, max_attempts + 1):
        response = requests.get(
            f"{BASE_URL}/evaluations/{task_id}/status",
            headers={"X-API-Key": API_KEY},
            timeout=60,
        )
        response.raise_for_status()
        payload = response.json()
        data = payload.get("data", payload)
        status = data["status"]
        if status in {"SUCCESS", "FAILURE", "CANCEL"}:
            return payload
        delay_seconds = min(initial_delay_seconds * attempt, max_delay_seconds)
        time.sleep(delay_seconds)

    raise TimeoutError(
        f"评测任务 {task_id} 在 {max_attempts} 次轮询后仍未结束"
    )

排查清单

  • 确认请求地址是 /api/keath/public/v1/...
  • 确认 Header 用的是 X-API-Key,不是 X-TOKEN-KEY
  • 确认你使用的是 KEATH Public API key,即 kct_...,不是 NewAPI sk-... 这类模型供应商 key
  • 确认 model_id 来自 GET /models
  • 确认 assignment_id 来自 POST /assignments 或产品 assignment 详情页 URL,不是来自 GET /models
  • 确认创建 assignment 或 one-pass evaluation 前,所选 model 已经是 ready
  • 如果上传学生作答文件,确认字段名使用 answer_file 等受支持字段
  • 如果是 one-pass 请求,确认 model_id 来自 GET /models
  • 如果 multipart 里同时传了 assets,确认它是一个 JSON 数组字符串
  • 如果 multipart 评测请求里传了 rubrics 或 current_feedbacks,确认它们是 JSON 数组字符串
  • 上传文件时优先显式传支持的 MIME type;如果客户端传不了,就确保文件扩展名是支持的格式
  • 确认每个文件都小于 20 MB
  • Google Docs 需要先导出成 DOCX、PDF 或纯文本再上传;Public v1 还没有直接 Google Docs OAuth 导入
Last Updated: 7/15/26, 8:35 AM
Contributors: PJ
Prev
KEATH Public API v1
Next
从旧接口迁移