Skip to content

其他语言实现 ​

ai-profile 目前只有 Rust 实现,但它的价值大半与语言无关:预置数据是一份 JSON,地址拼接、错误判定、 模型清洗、ai.profile 解析是一组规则。所以我们把这两样单独发布出来 —— 别的语言不用重抄数据、 不用凭文字猜规则:数据直接拿,规则照用例写,跑通用例就与 Rust 版行为一致。

这些文件由代码生成

全部由 crate 仓库的 cargo xtask gen-spec 从 Rust 参考实现现场算出,仓库内有守卫测试 spec_files_in_sync 保证它们与代码一致。每个文件头都标着产出它的 crate 版本。

三种配合方式 ​

你的需求做法工作量
只要服务商清单(地址、模型、密钥申请页)读 presets.json,按 kind 筛几十行
要完整能力(拼地址、测试连接、粘贴导入…)按实现范围写函数,跑通对应用例一到两天
同上,但想省事把用例交给 AI,让它照着写并跑通半天

三种方式都建议锁定版本(见下),升级时有意识地换,而不是被动跟着变。

下载 ​

文件内容
presets.json全部服务商预置 + 按厂商聚合的目录
conformance/endpoint.json端点拼接
conformance/model_filter.json模型清单清洗
conformance/models_response.json/models 响应解析(id 与限额)
conformance/diagnose.json验证失败的错误判定、必填字段检查
conformance/ai_profile.jsonai.profile 解析与生成
conformance/limits.jsontoken 限额三层合并

最新版与固定版本 ​

地址用途
https://ai-profile.ruoyi.plus/spec/…最新版,跟着 crate 发版变
https://ai-profile.ruoyi.plus/spec/v0.1.1/…固定版本,发布后不再改动。自动下载、写进构建脚本时用这个
/spec/versions.json已有版本清单:{"latest": "0.1.1", "versions": [...]}

最稳妥的做法是把整个目录复制进你的仓库(spec/),记下来源版本 —— 构建不依赖网络, 升级时整体替换、看哪些用例红了,红掉的就是这次规则变化的全部内容。

实现范围 ​

「必要性」按你要做的功能看:只做服务商下拉,前两行就够了。

功能要实现的函数(Rust 名)用例什么时候需要
预置数据presets / preset_by_key / vendors无,直接读 presets.json所有场景
端点拼接join_api_path / join_chat_endpoint / anthropic_base_urlendpoint.json发任何请求
模型清洗is_chat_model_id / clean_fetched_modelsmodel_filter.json做「获取模型」下拉
/models 解析parse_model_ids / parse_model_limitsmodels_response.json同上
错误判定diagnose / suggest_url / check_required_fieldsdiagnose.json做「测试连接」
测试连接verify无,按下面的流程把上面几个串起来做「测试连接」
导入导出parse_profiles / to_profileai_profile.json做粘贴导入、分享
限额TokenLimits::orlimits.json显示上下文窗口 / 输出上限
历史裁剪、生图 / 视频 / 配音调用history / media 模块暂无见未覆盖的部分

函数名、参数名在你的语言里按惯例改(join_api_path → joinApiPath),行为一致即可。

只用数据 ​

presets.json 里的 vendors 是全部 kind 的聚合。只做对话的应用应当先按 kind 筛预置,再按 vendorId 聚合 —— 直接用 vendors 的话,里面会混着「只提供视频」的厂商和你不支持的 presetKeys。

python
chat = [p for p in spec["presets"] if p["kind"] == "chat"]
by_vendor = {}
for p in chat:                       # 保持原数组顺序 = 分组顺序
    by_vendor.setdefault(p["vendorId"], []).append(p)

字段含义见预置与服务商目录。

测试连接的网络层 ​

verify 发网络请求,没法写成用例,但它只是把几个有用例的纯函数串起来。按这个顺序实现:

  1. 发请求前:check_required_fields(presetKey, extra),缺字段直接返回 missing_extra_field —— 更好的做法是用它禁用「测试」按钮。

  2. 定地址:表单填了 base_url 用表单的,没填用预置的;都为空返回 {"code": "missing_extra_field", "key": "base_url"}。 Anthropic 协议先过 anthropic_base_url,然后 url = join_api_path(base, "models")。

  3. 发 GET,超时 20 秒(连接 10 秒),禁止跟随重定向:

    协议请求头密钥为空时
    OpenAI 兼容Authorization: Bearer <key>不带这个头(本地 Ollama / vLLM 常不校验密钥)
    Anthropicx-api-key: <key> + anthropic-version: 2023-06-01只带 anthropic-version

    为什么必须禁重定向

    HTTP 库跨域跳转时通常只剥 Authorization 这类标准头,不剥自定义头 —— Anthropic 的 x-api-key 会被原样发给跳转目标。

  4. 连不上 / 超时 → {"code": "unreachable", "proxy_hint": …}。proxy_hint 表示「这个域名在国内通常要代理」, Rust 版对 api.openai.com、api.anthropic.com、generativelanguage.googleapis.com、openrouter.ai、 api.groq.com、api.x.ai 置为 true。

  5. 非 2xx → diagnose(status, body, url, base),其中 base 用第 2 步补过 /v1 的那个。

  6. 2xx → 组装成功结果:

    json
    {
      "latencyMs": 320,
      "models": ["…清洗后的清单"],
      "dropped": 2,
      "droppedModels": ["…被滤掉的"],
      "modelInList": true,
      "limits": { "contextWindow": 128000, "maxOutput": 8192, "source": "endpoint" },
      "modelLimits": [["deepseek-flash", { "contextWindow": 128000, "maxOutput": 8192, "source": "endpoint" }]]
    }
    • models / dropped / droppedModels:clean_fetched_models(parse_model_ids(body))
    • modelInList:当前填的 model 为空、或清单为空、或在清单里 → true。 模型不在清单里不算失败,界面据此提示「端点没有这个模型,要换一个吗」
    • limits:parse_model_limits(body) 里当前 model 那一条,没有就是 null
    • modelLimits:parse_model_limits(body) 的全部结果,每项是 [id, 限额]

错误对象的字段与各自该给用户的动作见错误码对照。

未覆盖的部分 ​

  • 历史裁剪(history):规则见历史裁剪与超长重试,目前没有用例, 请对照 Rust 源码的测试实现。这部分最容易出错的是「tool 调用与结果必须成对保留」。
  • 生图 / 视频 / 配音(media):本质是对各家 HTTP 协议的封装,每家请求格式不同,写成用例意义不大。 协议细节见生图、视频与配音与 crate 源码 src/media/。

文件格式 ​

每个文件都带同样的头:

json
{
  "specVersion": 1,
  "crateVersion": "0.1.1",
  "title": "端点拼接",
  "description": "规则摘要",
  "generatedBy": "cargo xtask gen-spec …",
  "cases": [ … ]
}
字段含义
specVersion格式版本。只在字段改名、结构调整时加 1;加用例、改期望值不算。遇到不认识的值应当拒绝运行,别按旧格式硬读
crateVersion生成这批用例的 crate 版本,期望值跟着它走
cases用例列表(presets.json 没有这一项,换成 presets 与 vendors)

每条用例:

json
{ "fn": "join_api_path", "name": "可选说明", "input": { "base": "…", "path": "models" }, "expected": "…" }
  • fn 对应一个要实现的函数,input 是参数(键名 camelCase),expected 是返回值。
  • 返回 Result 的函数,expected 是 {"ok": …} 或 {"error": …} 二选一。
  • expected 的键名照 Rust 版的线格式原样给出:数据结构是 camelCase,错误对象是 snake_case (如 suggested_url)。这是已发布的格式,照抄即可,别统一成一种。
  • 按 JSON 值比较(对象键无序),不要比字符串。to_profile 的输出也先解析再比。

接进你的测试 ​

以 Python + pytest 为例:

python
import json, pathlib, pytest
from my_ai_profile import join_api_path, join_chat_endpoint, anthropic_base_url

FNS = {
    "join_api_path": lambda i: join_api_path(i["base"], i["path"]),
    "join_chat_endpoint": lambda i: join_chat_endpoint(i["base"], i["path"]),
    "anthropic_base_url": lambda i: anthropic_base_url(i["base"]),
}
spec = json.loads(pathlib.Path("spec/conformance/endpoint.json").read_text("utf-8"))
assert spec["specVersion"] == 1

@pytest.mark.parametrize("case", spec["cases"], ids=lambda c: f'{c["fn"]}:{c["input"]}')
def test_endpoint(case):
    assert FNS[case["fn"]](case["input"]) == case["expected"]

TypeScript + Vitest 同理:

ts
import spec from './spec/conformance/endpoint.json'
import { joinApiPath, joinChatEndpoint, anthropicBaseUrl } from '../src/endpoint'

const fns: Record<string, (i: any) => unknown> = {
  join_api_path: (i) => joinApiPath(i.base, i.path),
  join_chat_endpoint: (i) => joinChatEndpoint(i.base, i.path),
  anthropic_base_url: (i) => anthropicBaseUrl(i.base),
}

test.each(spec.cases)('$fn $input', (c) => {
  expect(fns[c.fn](c.input)).toEqual(c.expected)
})

一条用例都别跳过。 跳过的那条往往正是规则里最反直觉的地方(比如 Anthropic 补 /v1 而 OpenAI 兼容不补)。

用 AI 实现 ​

用例本身就是最好的需求说明:AI 照着写、跑测试、看哪条红、再改,直到全绿。把下面这段连同用例文件一起交给它:

text
请用 <语言> 实现 ai-profile 的 <功能,如:端点拼接 + 获取模型 + 测试连接>。

规范与用例:
- 实现说明:https://ai-profile.ruoyi.plus/reference/spec.md
- 用例文件已放在本仓库 spec/ 目录(来自 https://ai-profile.ruoyi.plus/spec/v0.1.1/)

要求:
1. 先读实现说明的「实现范围」与「测试连接的网络层」两节,再动手
2. 为每个 conformance/*.json 写一个参数化测试,按 JSON 值比较 expected,一条都不许跳过
3. 不许修改用例文件来让测试通过;觉得某条期望值不合理就停下来告诉我
4. 预置数据运行时读 spec/presets.json,不要把服务商清单抄进代码
5. 全部用例通过后,列出你实现了哪些函数、哪些没实现(按实现说明里的表)

第 3 条最要紧:AI 遇到过不去的用例,最省事的做法就是改用例。

跟进更新 ​

  • 发版记录在 更新日志,也可以在 GitHub 上 Watch bkywksj/ai-profile 的 Releases。
  • 升级 = 把 spec/ 换成新版本目录 → 跑测试 → 修红掉的。
  • specVersion 变了说明格式有调整,先看更新日志再动手。
  • 预置数据(新增服务商、模型换代)是更新最频繁的部分;只用数据的场景,跟得越勤越好。

规则摘要 ​

用例是权威,这里只帮你读懂用例在测什么。各规则的设计理由见对应的 API 页。

规则要点详见
端点拼接OpenAI 兼容地址原样使用,不补 /v1;只剥掉误填的对话端点后缀与末尾 #。Anthropic 协议是唯一例外:末段不是 v<数字> 就补 /v1,末尾 # 表示别补端点与模型清单
模型清洗排除法:只滤掉带明确非对话特征的(向量、重排、语音、生图、OCR、审核…),未知名称一律放行;去空白、去重、保持顺序;全被滤光时原样返回端点与模型清单
/models 解析{"data":[…]} 与裸数组都接受;限额只收录报了的模型,OpenRouter 优先取 top_provider限额
错误判定401/403 → auth_failed;404 → not_found(看不出版本段时带 suggested_url);其余 → malformed。必填专有字段缺失 → missing_extra_field连通性验证
ai.profile解析宽进(多种字段拼写、单条与打包统一成列表、OAuth 条目跳过计数);生成严出(只产出规范写法)ai.profile 协议
限额合并用户 > 端点 > 预置,两两合并:高层全空时整条换成低层;否则逐字段补空,来源保留高层限额

model_not_found、protocol_mismatch 两个错误码已在格式里预留,但目前 Rust 版不会产生,实现时可以先不管。

边界 ​

  • 不要改用例去迁就实现。 觉得某条期望值不合理,请到 crate 仓库提 issue —— 规则改在 Rust 版, 重新生成后所有语言一起跟上。
  • 目前没有官方维护的其他语言实现。你写了一个,欢迎告诉我们,会列进本页。

相关 ​

MIT 协议开源 · 文档同样欢迎 PR