Reading protocol-specific model listings
读取协议特定的模型列表
The [draft provider interrogation](2026-08-04-draft-provider-endpoint-interrogation.md) originally read the OpenAI-compatible `data` array only. Some compatible gateways instead publish an enriched `models` object, while Anthropic publishes a native model-listing route with different authentication and URL rules. Treating either case as unsupported forced a user to copy model ids and capacities by hand even though th
English
Problem
The draft provider interrogation originally read the OpenAI-compatible data array only. Some compatible gateways instead publish an enriched models object, while Anthropic publishes a native model-listing route with different authentication and URL rules. Treating either case as unsupported forced a user to copy model ids and capacities by hand even though the endpoint disclosed them.
One gateway could be made to return an OpenAI-style array by sending an OpenAI SDK User-Agent. That behavior was undocumented, changed request attribution, and made the reply depend on a client identity rather than on a supported response parser.
Decision
dsh-llm-pi-ai reads model listings according to the selected protocol. openai-completions and openai-responses use GET {baseURL}/models with bearer authentication. anthropic-messages uses GET /v1/models?limit=1000 with x-api-key and anthropic-version: 2023-06-01. The Anthropic page size is the documented maximum; discovery does not follow has_more, so an endpoint advertising more than 1,000 models exposes only its first page.
Anthropic SDK resource methods append /v1 themselves, and gateway documentation publishes the API root both with and without that suffix. The listing URL therefore treats a drafted Anthropic baseURL ending in /v1 as the same API root as the address without it, and it is the only place that segment is normalized: model requests receive the configured baseURL unchanged, exactly as pi-ai handles it. Deployment path prefixes remain intact: https://gateway.example/tenant/v1 and https://gateway.example/tenant both list at /tenant/v1/models.
The parser accepts a data array or an enriched models object, with a present array taking precedence. Array entries use their id; object entries use the property key because a nested id may name a canonical model instead of the route alias accepted on requests. Only object-valued map entries are considered models, so primitive directory metadata cannot become a candidate accidentally. A nested id is the fallback for an empty property key.
The parser normalizes the supported name and capacity spellings into LlmDiscoveredModel. A missing display name becomes the request id so adoption fills a complete editable row. The request keeps the Harness attribution headers; response parsing, not client impersonation, provides gateway compatibility.
Alternatives considered
Follow every Anthropic page. Cursor traversal would return listings larger than 1,000 entries, but it adds multi-request failure, cancellation, cursor-progress, and aggregate-size behavior to a configuration action. The implementation requests Anthropic's maximum page and documents the remaining truncation.
Normalize the inference base as well. Stripping the same /v1 segment before model routing would let a /v1 address both list and serve, but it moves request URL rules out of pi-ai and into this package for one protocol. Model requests keep pi-ai's own handling of baseURL; the listing request is the only URL this package builds.
Refuse a trailing /v1. A load-time or discovery-time rejection would name the mistake early, but gateway documentation publishes the /v1 spelling, so a user pasting a documented address would be turned away from a listing that works.
Send an OpenAI SDK User-Agent for discovery. This made one gateway return data, but it misattributed Harness traffic and relied on an undocumented client-name branch. Reading both known reply formats keeps attribution accurate.
Adopt every property of a models object. A primitive-valued property does not prove that its key is a model id and may be directory metadata such as a count or status. Restricting entries to records avoids inventing model candidates.
Consequences
The Models page can interrogate OpenAI-compatible gateways and Anthropic Messages endpoints without changing request identity. Discovered candidates carry route ids, names, context windows, and output-token caps when the endpoint provides them, and name-only listings still receive an editable label through the id fallback. Anthropic addresses list in either root or /v1 form; model requests use the configured address as pi-ai receives it.
The supported formats remain an explicit compatibility set rather than arbitrary JSON inference. Anthropic accounts with more than 1,000 visible models require hand-entry for entries outside the first page, and primitive-valued models properties are ignored.
Testing
Local HTTP-server tests pin both accepted response formats, field normalization, name fallback, ignored malformed entries, Anthropic headers, the maximum-page query, and both spellings of the Anthropic root. Replies recorded from OpenRouter, models.dev, and DeepSeek on 2026-09-02, together with the example reply in Anthropic's List Models reference, live under packages/llm/llm-pi-ai/tests/fixtures/model-listings/ and replay through the parser, so the accepted field spellings are pinned to real endpoints and the published reference rather than to hand-written samples.
中文
问题
提供方草稿询问决策最初只读取 OpenAI 兼容的 data 数组。一些兼容网关改为公布富信息 models 对象,而 Anthropic 公布了具有不同认证与 URL 规则的原生模型列表路由。把任一情况视为不受支持,都会迫使用户手工复制模型 id 和容量,即使端点已经公布这些信息。
向一个网关发送 OpenAI SDK User-Agent 可以使其返回 OpenAI 风格数组。该行为没有文档,改变了请求归属,并使回答取决于客户端身份而非受支持的响应解析器。
决策
dsh-llm-pi-ai 按所选协议格式读取模型列表。openai-completions 与 openai-responses 以 bearer 认证使用 GET {baseURL}/models。anthropic-messages 以 x-api-key 和 anthropic-version: 2023-06-01 使用 GET /v1/models?limit=1000。Anthropic 页大小采用文档规定的最大值;模型发现不会继续跟随 has_more,因此公布超过 1,000 个模型的端点只会暴露第一页。
Anthropic SDK 资源方法会自行追加 /v1,而网关文档会同时发布带与不带该后缀的 API 根地址。因此列表 URL 会把末尾为 /v1 的 Anthropic baseURL 草稿视为与不带该后缀的地址相同的 API 根地址,并且只有它会归一化这一段:模型请求收到的仍是配置原样的 baseURL,与 pi-ai 的处理完全一致。部署路径前缀会保留:https://gateway.example/tenant/v1 与 https://gateway.example/tenant 都在 /tenant/v1/models 列表。
解析器接受 data 数组或富信息 models 对象,并在数组存在时优先使用它。数组条目使用自身的 id;对象条目使用属性键,因为嵌套 id 可能指向规范模型,而不是请求所接受的路由别名。只有值为对象的映射条目才视为模型,因此原始类型的目录元数据不会意外成为候选。空属性键才会回退到嵌套 id。
解析器会把受支持的名称与容量拼写归一化为 LlmDiscoveredModel。缺失的显示名会变成请求 id,使采纳操作填入完整的可编辑行。请求保留 Harness 归属标头;网关兼容性由响应解析提供,而非冒充客户端身份。
考虑过的替代方案
跟随 Anthropic 的所有页面。 游标遍历可以返回超过 1,000 个条目的列表,但会给配置操作增加多请求失败、取消、游标推进与总大小处理。实现请求 Anthropic 的最大页面,并记录剩余截断限制。
同时归一化推理地址。 在模型路由前截掉同一段 /v1 可以让 /v1 地址既能列表也能服务,但这会把请求 URL 规则从 pi-ai 挪进本包,且只为一种协议。模型请求保持 pi-ai 自身对 baseURL 的处理;列表请求是本包构造的唯一 URL。
拒绝末尾的 /v1。 在加载或探测时拒绝可以尽早点出错误,但网关文档发布的就是 /v1 写法,照文档粘贴地址的用户会被一个本能工作的列表拒之门外。
为模型发现发送 OpenAI SDK User-Agent。 这会让一个网关返回 data,但会错误标记 Harness 流量,并依赖未记录的客户端名称分支。读取两种已知响应格式可以保持归属准确。
采纳 models 对象的每个属性。 原始类型属性不能证明其键是模型 id,也可能是数量或状态等目录元数据。把条目限制为记录可避免虚构模型候选。
后果
Models 页面无需改变请求身份,即可询问 OpenAI 兼容网关与 Anthropic Messages 端点。发现的候选会在端点提供时携带路由 id、名称、上下文窗口与最大输出 token 数,只有 id 的列表也会通过 id 回退获得可编辑标签。Anthropic 地址以根地址或 /v1 形式都能列表;模型请求使用 pi-ai 收到的配置地址。
受支持格式仍是显式兼容集合,而不是任意 JSON 推断。可见模型超过 1,000 个的 Anthropic 账户需要手工录入第一页之外的条目,原始类型的 models 属性会被忽略。
测试
本地 HTTP 服务器测试钉住两种受支持响应格式、字段归一化、名称回退、忽略畸形条目、Anthropic 标头、最大页查询,以及 Anthropic 根地址的两种写法。2026-09-02 从 OpenRouter、models.dev 与 DeepSeek 录得的回复,连同 Anthropic List Models 参考文档给出的示例回复,存放在 packages/llm/llm-pi-ai/tests/fixtures/model-listings/ 下并经解析器回放,因此受支持的字段拼写钉在真实端点与公开参考文档上,而不是手写样例上。