@weiliang0121/capix-model-provider 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +112 -2
- package/dist/contract.d.ts +9 -0
- package/dist/contract.js +2 -0
- package/dist/error.d.ts +10 -0
- package/dist/error.js +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/provider.d.ts +9 -0
- package/dist/provider.js +2 -0
- package/dist/response.d.ts +2 -0
- package/dist/response.js +2 -0
- package/dist/stream.d.ts +2 -0
- package/dist/stream.js +5 -0
- package/dist/types.d.ts +95 -0
- package/dist/types.js +1 -0
- package/dist/usage.d.ts +2 -0
- package/dist/usage.js +2 -0
- package/dist/value.d.ts +8 -0
- package/dist/value.js +2 -0
- package/dist/wire.d.ts +3 -0
- package/dist/wire.js +2 -0
- package/package.json +37 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wei.liang (https://github.com/weiliang0121)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,113 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 模型 Provider
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@weiliang0121/capix-model-provider` 提供 Chat Completions 文本与原生函数调用、可选 SSE,以及唯一的 `ModelReply` 纯值契约。可以从 `.mjs` 使用,不依赖 Runtime、资源、Interaction 或服务。Provider 负责通信和协议接纳;工具选择、参数业务校验、执行、接纳与恢复由调用方组织。
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { ChatCompletionsProvider, modelReplyText } from '@weiliang0121/capix-model-provider';
|
|
7
|
+
|
|
8
|
+
const model = new ChatCompletionsProvider({
|
|
9
|
+
endpoint: 'https://api.deepseek.com/chat/completions',
|
|
10
|
+
apiKey: process.env.DEEPSEEK_API_KEY,
|
|
11
|
+
model: process.env.DEEPSEEK_MODEL || 'deepseek-flash',
|
|
12
|
+
thinking: 'disabled',
|
|
13
|
+
});
|
|
14
|
+
const reply = await model.complete({
|
|
15
|
+
messages: [{ role: 'user', content: '把 hello 翻译为中文。' }],
|
|
16
|
+
maxTokens: 512,
|
|
17
|
+
});
|
|
18
|
+
if (reply.termination.reason !== 'stop') throw new Error('模型未正常完成');
|
|
19
|
+
console.log(modelReplyText(reply));
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
通过 `npm install @weiliang0121/capix-model-provider` 安装后可直接运行;仅在本仓库从源码使用时需要先执行 `pnpm build`。配置由宿主传入,包不读取环境文件。只有显式执行真实请求才会访问模型。
|
|
23
|
+
|
|
24
|
+
## 唯一回复材料
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
interface ModelReply {
|
|
28
|
+
formatVersion: 1;
|
|
29
|
+
id: string;
|
|
30
|
+
model: string;
|
|
31
|
+
output: readonly ({ kind: 'text'; text: string } | { kind: 'tool-call'; id: string; name: string; arguments: { encoding: 'json-text'; text: string } })[];
|
|
32
|
+
termination: {
|
|
33
|
+
kind: 'completed' | 'incomplete' | 'refused';
|
|
34
|
+
reason: string;
|
|
35
|
+
};
|
|
36
|
+
usage: ModelUsage | null;
|
|
37
|
+
native: {
|
|
38
|
+
format: 'chat-completions';
|
|
39
|
+
version: 1;
|
|
40
|
+
reasoningContent?: string;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
共同字段保存正文、工具调用、结束原因和用量;`native` 只保存共同字段表达不了的思考文本。`format` 描述协议,不充当厂商身份。Provider 从这份材料编码下一次请求,没有可分别修改的第二份 continuation。工具参数保留原始 JSON 字符串,即使字符串不能被 JSON 解析也不会擅自修复;是否注册、参数是否合法由工具调用方判断。
|
|
46
|
+
|
|
47
|
+
`stop` 与 `tool_calls` 映射为 `completed`,`content_filter` 为 `refused`,其他原因(包括未知新值)保留原文并映射为 `incomplete`。`completed` 表示协议结束,并不表示业务已完成。`modelReplyText(reply)` 拒绝含工具调用的回复;它不代替业务检查 `stop`、拒绝状态或非空正文。
|
|
48
|
+
|
|
49
|
+
公开 `requireModelReply`、`requireModelRequest`、`requireToolResult`、`requireToolFeedback` 和 `requireModelExchange` 对未知值作完整校验,返回原对象身份,不克隆、不冻结,也不缓存对可变对象的信任。适用于资源、Interaction、实体与 CP 的接纳边界。校验拒绝旧双字段形状、未知字段/版本、重复 ID、不完整反馈、getter、Proxy、循环引用与非 JSON 值;错误不包含传入正文。Provider 自己生成的回复已深冻结。旧 `text/finishReason` 双字段回复不是新协议的有效持久值,不能自动迁移后冒充已认证反馈。
|
|
50
|
+
|
|
51
|
+
## 原生工具与续接
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
const tools = [
|
|
55
|
+
{
|
|
56
|
+
name: 'read_file',
|
|
57
|
+
description: '读取一个已授权文件。',
|
|
58
|
+
parameters: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: { path: { type: 'string' } },
|
|
61
|
+
required: ['path'],
|
|
62
|
+
additionalProperties: false,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
const request = { messages: [{ role: 'user', content: '读取 notes.md。' }], tools };
|
|
67
|
+
const first = await model.complete(request);
|
|
68
|
+
|
|
69
|
+
// 业务负责核验结束状态、参数、权限与执行;此处只展示反馈契约。
|
|
70
|
+
const feedback = first.output
|
|
71
|
+
.filter((item) => item.kind === 'tool-call')
|
|
72
|
+
.map((call) => ({
|
|
73
|
+
toolCallId: call.id,
|
|
74
|
+
status: 'ok',
|
|
75
|
+
content: [{ kind: 'text', text: '经授权工具读取的内容' }],
|
|
76
|
+
}));
|
|
77
|
+
const next = await model.complete({
|
|
78
|
+
...request,
|
|
79
|
+
exchanges: [{ reply: first, feedback }],
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`messages` 是固定的 system/user/assistant 文本前缀,`exchanges` 按顺序附加完整的模型交换。每个交换必须有准确且完整的反馈集合;Provider 按模型原调用顺序编码 `assistant.tool_calls` 与 `tool.tool_call_id`,调用方反馈数组的排列不改变关联。`ToolResult` 为 `{status:'ok'|'error',content:[{kind:'text',text}]}`,`ToolFeedback` 另含 `toolCallId`;两种状态都编码为明确的 JSON 数据正文,不能靠措辞暗示失败。工具结果不会成为 system 消息,Provider 也不自动执行其中指令。结果的可信程度、提示策略与实际资源授权仍由宿主和业务确定。
|
|
84
|
+
|
|
85
|
+
`toolChoice` 可为 `auto`、`none`、`required` 或 `{name}`。指定名称必须已登记;除 `none` 外,显式选择要求有工具目录。参数 Schema 为纯 JSON 对象,原样转交,不在 Provider 内建立另一套业务 Schema 执行器。本实现每份目录/回复最多 32 个工具,工具名支持字母、数字、`_`、`-`,长度 1–128。
|
|
86
|
+
|
|
87
|
+
DeepSeek 思考模式在带 `tools` 的后续请求中要求回传此前回复的 `reasoning_content`,包括没有工具调用的回复。Provider 在 exchanges 中始终保留并编码该字段;不带 tools 时,是否使用由兼容端点决定。调用方不能用只有正文的普通 assistant 消息替代需要精确续接的思考回复。协议依据:[工具调用](https://api-docs.deepseek.com/guides/tool_calls/)、[思考模式](https://api-docs.deepseek.com/guides/thinking_mode/)。
|
|
88
|
+
|
|
89
|
+
## 流式与用量
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
const reply = await model.complete({ messages: [{ role: 'user', content: '解释这份材料。' }], stream: true }, { signal, emit: (delta) => process.stdout.write(delta) });
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`stream` 默认 false;单独提供 emit 不开启流式。emit 只同步交付正文预览,工具参数与思考文本不会被当作正文发布。未提供 emit 时仍完整消费。回调抛错保留原错误身份,回调返回 Promise 会被拒绝。预览不代表最终回复或业务接纳已成立。
|
|
96
|
+
|
|
97
|
+
SSE 按 `tool_calls[index]` 拼接名称与参数片段,保留 reasoning_content,终态与独立 usage 均支持。缺少 ID、重复 ID、索引缺口、超过 32 个调用、终态后继续 choice、重复 usage、缺少 finish_reason 或 `[DONE]` 均拒绝。`[DONE]` 结束协议后立即释放读取,不要求上游 HTTP 连接先关闭;不重发或退回非流式请求。原有 UTF-8 分片、SSE 换行/注释、多行 data、接收者失败、取消和总超时行为保持。
|
|
98
|
+
|
|
99
|
+
JSON 与 SSE 共用 `ModelUsage`:`promptTokens / completionTokens / totalTokens`,可选 `promptCacheHitTokens / promptCacheMissTokens` 原样保留。计数必须是非负安全整数,缓存单项不超过 promptTokens,两项都报告时其和须等于 promptTokens。缺失不估算为零;不把其他厂商的缓存字段自动映射成 DeepSeek 计数。
|
|
100
|
+
|
|
101
|
+
## 容量与范围
|
|
102
|
+
|
|
103
|
+
`ChatCompletionsProvider.inspectRequest(request)` 同步完成请求预检,返回冻结的 `{requestBytes, wireBytes}`:前者是共同请求完整 JSON 的 UTF-8 字节数,后者是实际 HTTP 正文字节数,包括固定的 model/thinking 配置、工具目录、交换编码和流式字段。它与 `complete` 共用校验和编码路径,能够计算 Unicode、转义字符以及工具反馈嵌套 JSON 带来的真实大小;不发请求、不读取网络,也不返回正文、headers 或密钥。
|
|
104
|
+
|
|
105
|
+
`ModelProvider` 上此端口可选,依赖精确发送预算的宿主应在装配时要求它存在。无效或超出共同请求/HTTP 正文 2 MiB 限额时,预检与 `complete` 一样抛出 `ProviderError('REQUEST')`。预检不冻结输入、不预留额度,也不保证模型上下文 token 容量;实际发送仍重新校验当前请求,修改过输入后必须以本次检查为准。
|
|
106
|
+
|
|
107
|
+
- 公开请求、回复、反馈、交换值均限制为 2 MiB 的纯 JSON,深度最多 64、值数最多 100,000。请求编码后的 HTTP 正文也受 2 MiB 限制。合法的单项不代表任意累计历史都能继续装入请求。
|
|
108
|
+
- 默认总超时 60 秒,涵盖发送与完整读取。`maxResponseBytes` 是独立的实际传输预算,默认 2 MiB,SSE 计入事件封装、注释和思考内容;提高传输预算不会放宽公开回复的纯值上限。
|
|
109
|
+
- 一次 complete 只有一次 HTTP 请求,无隐式重试。调用取消保持原 reason,不修改调用者 signal;对独立 Interaction 的生命周期不作推断。
|
|
110
|
+
- `ProviderError` 只含 code/message/可选 HTTP status,不保存上游正文、请求、headers、密钥或底层错误。宿主可注入 fetch;实现必须遵守 Fetch 取消与重定向语义。
|
|
111
|
+
- `thinking` 是宿主显式配置的 DeepSeek 扩展。当前实现 Chat Completions 函数调用与文本;不承诺 Responses、Anthropic 原生协议、MCP、多模态或 strict beta。正式 Transcript 的消息角色不因此扩展。
|
|
112
|
+
|
|
113
|
+
当前架构与限制见[原生工具调用](../../docs/native-tool-integration.md)。完整协议参考 [Chat Completions](https://api-docs.deepseek.com/api/create-chat-completion/) 与 [SSE 标准](https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation)。
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ModelExchange, ModelReply, ModelRequest, ToolFeedback, ToolResult } from './types.js';
|
|
2
|
+
export declare function terminationKind(reason: string): ModelReply['termination']['kind'];
|
|
3
|
+
export declare function requireModelReply(value: unknown): ModelReply;
|
|
4
|
+
export declare function requireToolResult(value: unknown): ToolResult;
|
|
5
|
+
export declare function requireToolFeedback(value: unknown): ToolFeedback;
|
|
6
|
+
export declare function requireModelExchange(value: unknown): ModelExchange;
|
|
7
|
+
export declare function requireModelRequest(value: unknown): ModelRequest;
|
|
8
|
+
export declare function measureModelRequest(value: unknown): number;
|
|
9
|
+
export declare function modelReplyText(reply: ModelReply): string;
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
|
|
2
|
+
var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{count,identity,record,requireJson,shape,toolLimit}from"./value.js";function terminationKind(reason){return reason==="stop"||reason==="tool_calls"?"completed":reason==="content_filter"?"refused":"incomplete"}__name(terminationKind,"terminationKind");function requireModelReply(value){return requireJson(value),checkReply(value),value}__name(requireModelReply,"requireModelReply");function checkReply(value){if(shape(value,["formatVersion","id","model","output","termination","usage","native"]),value.formatVersion!==1)throw new TypeError("Unsupported model reply version");if(identity(value.id),identity(value.model),shape(value.termination,["kind","reason"]),identity(value.termination.reason),value.termination.kind!==terminationKind(value.termination.reason))throw new TypeError("Model termination does not match its reason");if(shape(value.native,["format","version"],["reasoningContent"]),value.native.format!=="chat-completions"||value.native.version!==1||"reasoningContent"in value.native&&typeof value.native.reasoningContent!="string")throw new TypeError("Unsupported model native material");if(!Array.isArray(value.output)||value.output.length>toolLimit+1)throw new TypeError("Model output is invalid");let text=!1;const calls=new Set;for(const item of value.output)if(record(item)&&item.kind==="text"){if(checkText(item),text||calls.size)throw new TypeError("Unsupported Chat Completions output order");text=!0}else{if(shape(item,["kind","id","name","arguments"]),item.kind!=="tool-call")throw new TypeError("Unsupported model output kind");if(identity(item.id),toolName(item.name),shape(item.arguments,["encoding","text"]),item.arguments.encoding!=="json-text"||typeof item.arguments.text!="string")throw new TypeError("Unsupported tool argument representation");if(calls.has(item.id)||calls.size>=toolLimit)throw new TypeError("Duplicate or excessive model tool calls");calls.add(item.id)}if(value.termination.reason==="tool_calls"&&!calls.size||value.termination.reason==="stop"&&calls.size)throw new TypeError("Model termination disagrees with tool output");if(value.usage!==null){shape(value.usage,["promptTokens","completionTokens","totalTokens"],["promptCacheHitTokens","promptCacheMissTokens"]);const{promptTokens,completionTokens,totalTokens,promptCacheHitTokens:hit,promptCacheMissTokens:miss}=value.usage;if(!count(promptTokens)||!count(completionTokens)||!count(totalTokens)||hit!==void 0&&(!count(hit)||hit>promptTokens)||miss!==void 0&&(!count(miss)||miss>promptTokens)||typeof hit=="number"&&typeof miss=="number"&&hit!==promptTokens-miss)throw new TypeError("Invalid model usage counts")}}__name(checkReply,"checkReply");function checkText(value){if(shape(value,["kind","text"]),value.kind!=="text"||typeof value.text!="string")throw new TypeError("Tool content must contain text blocks")}__name(checkText,"checkText");function toolName(value){if(typeof value!="string"||!/^[A-Za-z0-9_-]{1,128}$/u.test(value))throw new TypeError("Invalid tool name")}__name(toolName,"toolName");function checkResult(value,feedback){if(shape(value,feedback?["toolCallId","status","content"]:["status","content"]),feedback&&identity(value.toolCallId),typeof value.status!="string"||!["ok","error"].includes(value.status)||!Array.isArray(value.content)||!value.content.length||value.content.length>32)throw new TypeError("Invalid tool feedback content");for(const item of value.content)checkText(item)}__name(checkResult,"checkResult");function requireToolResult(value){return requireJson(value),checkResult(value,!1),value}__name(requireToolResult,"requireToolResult");function requireToolFeedback(value){return requireJson(value),checkResult(value,!0),value}__name(requireToolFeedback,"requireToolFeedback");function checkExchange(value){if(shape(value,["reply","feedback"]),checkReply(value.reply),value.reply.termination.kind!=="completed")throw new TypeError("Incomplete or refused reply cannot be continued as a closed exchange");if(!Array.isArray(value.feedback))throw new TypeError("Tool feedback must be an array");const calls=value.reply.output.filter(item=>item.kind==="tool-call");if(value.feedback.length!==calls.length)throw new TypeError("Tool feedback does not close the model exchange");const ids=new Set;for(const feedback of value.feedback){checkResult(feedback,!0);const id=feedback.toolCallId;if(ids.has(id)||!calls.some(call=>call.id===id))throw new TypeError("Tool feedback has a duplicate or foreign call identity");ids.add(id)}}__name(checkExchange,"checkExchange");function requireModelExchange(value){return requireJson(value),checkExchange(value),value}__name(requireModelExchange,"requireModelExchange");function requireModelRequest(value){return measureModelRequest(value),value}__name(requireModelRequest,"requireModelRequest");function measureModelRequest(value){const bytes=requireJson(value);return checkRequest(value),bytes}__name(measureModelRequest,"measureModelRequest");function checkRequest(value){if(shape(value,["messages"],["exchanges","tools","toolChoice","maxTokens","responseFormat","temperature","stream"]),!Array.isArray(value.messages)||!value.messages.length)throw new TypeError("At least one model message is required");for(const message of value.messages)if(shape(message,["role","content"]),typeof message.role!="string"||!["system","user","assistant"].includes(message.role)||typeof message.content!="string")throw new TypeError("Model message has an unsupported role or content");if(value.exchanges!==void 0){if(!Array.isArray(value.exchanges))throw new TypeError("Model exchanges must be an array");for(const exchange of value.exchanges)checkExchange(exchange)}const names=new Set;if(value.tools!==void 0){if(!Array.isArray(value.tools)||!value.tools.length||value.tools.length>toolLimit)throw new TypeError("Model tool catalog must contain 1 to 32 tools");for(const tool of value.tools){if(shape(tool,["name","description","parameters"]),toolName(tool.name),names.has(tool.name)||typeof tool.description!="string"||!record(tool.parameters))throw new TypeError("Invalid or duplicate model tool declaration");names.add(tool.name)}}if(value.toolChoice!==void 0){if(record(value.toolChoice)){if(shape(value.toolChoice,["name"]),toolName(value.toolChoice.name),!names.has(value.toolChoice.name))throw new TypeError("Selected model tool is not declared")}else if(typeof value.toolChoice!="string"||!["auto","none","required"].includes(value.toolChoice))throw new TypeError("Unsupported model tool choice");if(value.toolChoice!=="none"&&!names.size)throw new TypeError("Model tool choice requires tools")}if(value.maxTokens!==void 0&&(!count(value.maxTokens)||value.maxTokens<1))throw new TypeError("maxTokens must be a positive integer");if(value.temperature!==void 0&&(typeof value.temperature!="number"||!Number.isFinite(value.temperature)||value.temperature<0||value.temperature>2))throw new TypeError("temperature must be between zero and two");if(value.responseFormat!==void 0&&(typeof value.responseFormat!="string"||!["text","json_object"].includes(value.responseFormat)))throw new TypeError("Unsupported response format");if(value.stream!==void 0&&typeof value.stream!="boolean")throw new TypeError("stream must be a boolean")}__name(checkRequest,"checkRequest");function modelReplyText(reply){let text="";for(const item of reply.output){if(item.kind==="tool-call")throw new TypeError("Tool-call output cannot be projected as a text reply");text+=item.text}return text}__name(modelReplyText,"modelReplyText");export{measureModelRequest,modelReplyText,requireModelExchange,requireModelReply,requireModelRequest,requireToolFeedback,requireToolResult,terminationKind};
|
package/dist/error.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class ProviderError extends Error {
|
|
2
|
+
readonly code: 'CONFIGURATION' | 'REQUEST' | 'NETWORK' | 'HTTP' | 'TIMEOUT' | 'RESPONSE_LIMIT' | 'ENVELOPE';
|
|
3
|
+
readonly status?: number | undefined;
|
|
4
|
+
constructor(code: 'CONFIGURATION' | 'REQUEST' | 'NETWORK' | 'HTTP' | 'TIMEOUT' | 'RESPONSE_LIMIT' | 'ENVELOPE', message: string, status?: number | undefined);
|
|
5
|
+
}
|
|
6
|
+
export declare class ModelEmissionFailure {
|
|
7
|
+
readonly failure: unknown;
|
|
8
|
+
constructor(failure: unknown);
|
|
9
|
+
}
|
|
10
|
+
export declare function isRequestCancellation(error: unknown, signal: AbortSignal): boolean;
|
package/dist/error.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
|
|
2
|
+
var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});class ProviderError extends Error{static{__name(this,"ProviderError")}code;status;constructor(code,message,status){super(message),this.code=code,this.status=status,this.name="ProviderError"}}class ModelEmissionFailure{static{__name(this,"ModelEmissionFailure")}failure;constructor(failure){this.failure=failure}}function isRequestCancellation(error,signal){return signal.aborted&&(error===signal.reason||error instanceof DOMException&&error.name==="AbortError")}__name(isRequestCancellation,"isRequestCancellation");export{ModelEmissionFailure,ProviderError,isRequestCancellation};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { ChatCompletionsProvider } from './provider.js';
|
|
2
|
+
export { ProviderError } from './error.js';
|
|
3
|
+
export { requireModelReply, requireModelRequest, requireModelExchange, requireToolResult, requireToolFeedback, modelReplyText } from './contract.js';
|
|
4
|
+
export type { ChatCompletionsConfiguration, ModelControls, ModelMessage, ModelProvider, ModelReply, ModelRequest, ModelRequestInspection, ModelOutput, ModelTextOutput, ModelToolCall, ModelUsage, ModelExchange, ModelTool, ModelToolChoice, ToolResult, ToolFeedback, JsonObject, JsonValue } from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
|
|
2
|
+
import{ChatCompletionsProvider}from"./provider.js";import{ProviderError}from"./error.js";import{requireModelReply,requireModelRequest,requireModelExchange,requireToolResult,requireToolFeedback,modelReplyText}from"./contract.js";export{ChatCompletionsProvider,ProviderError,modelReplyText,requireModelExchange,requireModelReply,requireModelRequest,requireToolFeedback,requireToolResult};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ChatCompletionsConfiguration, ModelControls, ModelProvider, ModelReply, ModelRequest, ModelRequestInspection } from './types.js';
|
|
2
|
+
export declare class ChatCompletionsProvider implements ModelProvider {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(configuration: ChatCompletionsConfiguration);
|
|
5
|
+
inspectRequest(request: ModelRequest): ModelRequestInspection;
|
|
6
|
+
complete(request: ModelRequest, controls?: ModelControls): Promise<ModelReply>;
|
|
7
|
+
private prepareRequest;
|
|
8
|
+
private requestBody;
|
|
9
|
+
}
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
|
|
2
|
+
var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{isRequestCancellation,ModelEmissionFailure,ProviderError}from"./error.js";import{readModelReply}from"./response.js";import{readModelStream}from"./stream.js";import{measureModelRequest}from"./contract.js";import{encodeMessages}from"./wire.js";import{valueLimit}from"./value.js";class ChatCompletionsProvider{static{__name(this,"ChatCompletionsProvider")}#configuration;#fetch;constructor(configuration){let endpoint;try{endpoint=new URL(configuration.endpoint)}catch{throw new ProviderError("CONFIGURATION","A complete model HTTP endpoint is required")}if(!["http:","https:"].includes(endpoint.protocol)||endpoint.username||endpoint.password||endpoint.hash)throw new ProviderError("CONFIGURATION","A plain HTTP endpoint without embedded credentials or fragment is required");if(typeof configuration.apiKey!="string"||!configuration.apiKey.trim()||/[\r\n]/.test(configuration.apiKey))throw new ProviderError("CONFIGURATION","A model API key is required");if(typeof configuration.model!="string"||!configuration.model.trim())throw new ProviderError("CONFIGURATION","A model name is required");const timeoutMs=configuration.timeoutMs??6e4,maxResponseBytes=configuration.maxResponseBytes??2*1024*1024;if(!Number.isSafeInteger(timeoutMs)||timeoutMs<1||timeoutMs>2147483647||!Number.isSafeInteger(maxResponseBytes)||maxResponseBytes<1)throw new ProviderError("CONFIGURATION","Positive bounded timeout and response byte limit are required");if(configuration.thinking!==void 0&&!["enabled","disabled"].includes(configuration.thinking))throw new ProviderError("CONFIGURATION","Unsupported thinking setting");if(configuration.fetch!==void 0&&typeof configuration.fetch!="function")throw new ProviderError("CONFIGURATION","Fetch transport must be a function");this.#configuration=Object.freeze({endpoint:endpoint.href,apiKey:configuration.apiKey,model:configuration.model,timeoutMs,maxResponseBytes,thinking:configuration.thinking}),this.#fetch=configuration.fetch??globalThis.fetch}inspectRequest(request){return this.prepareRequest(request).inspection}async complete(request,controls={}){const external=controls.signal;external?.throwIfAborted();const emit=controls.emit;if(emit!==void 0&&typeof emit!="function")throw new ProviderError("REQUEST","Model preview consumer must be a function");const{body,streaming}=this.prepareRequest(request),controller=new AbortController,abort=__name(()=>controller.abort(external?.reason),"abort");external?.addEventListener("abort",abort,{once:!0}),external?.aborted&&abort();const timeout=setTimeout(()=>controller.abort(new ProviderError("TIMEOUT","Model request exceeded its total timeout")),this.#configuration.timeoutMs);try{controller.signal.throwIfAborted();const response=await this.#fetch(this.#configuration.endpoint,{method:"POST",redirect:"error",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.#configuration.apiKey}`},body,signal:controller.signal});if(controller.signal.aborted&&(await response.body?.cancel().catch(()=>{}),controller.signal.throwIfAborted()),!response.ok){const failure=new ProviderError("HTTP",`Model endpoint returned HTTP ${response.status}; request was not retried`,response.status);throw await response.body?.cancel().catch(()=>{}),failure}return streaming?await readModelStream(response,controller.signal,this.#configuration.maxResponseBytes,emit):await readModelReply(response,controller.signal,this.#configuration.maxResponseBytes)}catch(error){throw error instanceof ModelEmissionFailure?error.failure:isRequestCancellation(error,controller.signal)?controller.signal.reason:error instanceof ProviderError?error:new ProviderError("NETWORK","Model transport failed; request was not retried")}finally{clearTimeout(timeout),external?.removeEventListener("abort",abort)}}prepareRequest(request){try{const requestBytes=measureModelRequest(request),streaming=request.stream===!0,body=this.requestBody(request,streaming),wireBytes=Buffer.byteLength(body);if(wireBytes>valueLimit)throw new TypeError("Model wire request exceeds byte limit");return{body,streaming,inspection:Object.freeze({requestBytes,wireBytes})}}catch{throw new ProviderError("REQUEST","Model request has invalid, unsupported or excessive content")}}requestBody(request,stream){const messages=encodeMessages(request),tools=request.tools?.map(tool=>({type:"function",function:{name:tool.name,description:tool.description,parameters:tool.parameters}})),choice=request.toolChoice;return JSON.stringify({model:this.#configuration.model,messages,stream,...tools?{tools}:{},...choice===void 0?{}:{tool_choice:typeof choice=="string"?choice:{type:"function",function:{name:choice.name}}},...stream?{stream_options:{include_usage:!0}}:{},...request.maxTokens===void 0?{}:{max_tokens:request.maxTokens},...request.temperature===void 0?{}:{temperature:request.temperature},...request.responseFormat===void 0?{}:{response_format:{type:request.responseFormat}},...this.#configuration.thinking===void 0?{}:{thinking:{type:this.#configuration.thinking}}})}}export{ChatCompletionsProvider};
|
package/dist/response.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
|
|
2
|
+
var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{isRequestCancellation,ProviderError}from"./error.js";import{acceptWireReply}from"./wire.js";async function readModelReply(response,signal,limit){if(!response.body)throw envelope();const reader=response.body.getReader(),abort=__name(()=>{reader.cancel(signal.reason).catch(()=>{})},"abort");signal.addEventListener("abort",abort,{once:!0});let complete=!1;try{signal.throwIfAborted();const decoder=new TextDecoder("utf-8",{fatal:!0});let bytes=0,text="",chunk=await reader.read();for(;!chunk.done;){if(signal.throwIfAborted(),bytes+=chunk.value.byteLength,bytes>limit)throw new ProviderError("RESPONSE_LIMIT","Model response exceeds the configured byte limit");text+=decoder.decode(chunk.value,{stream:!0}),chunk=await reader.read()}return signal.throwIfAborted(),text+=decoder.decode(),complete=!0,acceptWireReply(JSON.parse(text))}catch(error){throw isRequestCancellation(error,signal)?signal.reason:error instanceof ProviderError?error:envelope()}finally{signal.removeEventListener("abort",abort),complete||await reader.cancel().catch(()=>{}),reader.releaseLock()}}__name(readModelReply,"readModelReply");function envelope(){return new ProviderError("ENVELOPE","Model response is not a supported Chat Completions envelope")}__name(envelope,"envelope");export{readModelReply};
|
package/dist/stream.d.ts
ADDED
package/dist/stream.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
|
|
2
|
+
var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{isRequestCancellation,ModelEmissionFailure,ProviderError}from"./error.js";import{acceptUsage}from"./usage.js";import{acceptWireReply}from"./wire.js";import{toolLimit}from"./value.js";async function readModelStream(response,signal,limit,emit){if(!response.body)throw envelope();if(response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase()!=="text/event-stream")throw await response.body.cancel().catch(()=>{}),envelope();const reader=response.body.getReader(),abort=__name(()=>{reader.cancel(signal.reason).catch(()=>{})},"abort");signal.addEventListener("abort",abort,{once:!0});try{signal.throwIfAborted();const completion=new ChatCompletionStream(signal,emit),events=new ServerSentEvents(data=>completion.receive(data)),decoder=new TextDecoder("utf-8",{fatal:!0});let bytes=0,chunk=await reader.read();for(;!chunk.done&&!completion.done;){if(signal.throwIfAborted(),bytes+=chunk.value.byteLength,bytes>limit)throw new ProviderError("RESPONSE_LIMIT","Model response exceeds the configured byte limit");events.write(decoder.decode(chunk.value,{stream:!0})),completion.done||(chunk=await reader.read())}if(signal.throwIfAborted(),!completion.done)throw events.write(decoder.decode()),envelope();return completion.result()}catch(error){throw error instanceof ModelEmissionFailure?error:isRequestCancellation(error,signal)?signal.reason:error instanceof ProviderError?error:envelope()}finally{signal.removeEventListener("abort",abort),await reader.cancel().catch(()=>{}),reader.releaseLock()}}__name(readModelStream,"readModelStream");class ServerSentEvents{static{__name(this,"ServerSentEvents")}receive;#line="";#skipLF=!1;#data=[];#done=!1;constructor(receive){this.receive=receive}write(text){let start=0;for(let index=0;index<text.length&&!this.#done;index++){const character=text[index];if(this.#skipLF&&(this.#skipLF=!1,character===`
|
|
3
|
+
`)){start=index+1;continue}character!==`
|
|
4
|
+
`&&character!=="\r"||(this.acceptLine(this.#line+text.slice(start,index)),this.#line="",this.#skipLF=character==="\r",start=index+1)}this.#done||(this.#line+=text.slice(start))}acceptLine(line){if(line===""){this.#data.length&&(this.#done=this.receive(this.#data.join(`
|
|
5
|
+
`))),this.#data=[];return}const colon=line.indexOf(":");if((colon===-1?line:line.slice(0,colon))!=="data")return;const value=colon===-1?"":line.slice(colon+1);this.#data.push(value.startsWith(" ")?value.slice(1):value)}}class ChatCompletionStream{static{__name(this,"ChatCompletionStream")}signal;emit;#id;#model;#finishReason;#usage=null;#wireUsage;#fragments=[];#hasContent=!1;#reasoning=[];#hasReasoning=!1;#calls=new Map;#done=!1;constructor(signal,emit){this.signal=signal,this.emit=emit}get done(){return this.#done}receive(data){if(this.signal.throwIfAborted(),data==="[DONE]"){if(!this.#id||!this.#model||!this.#finishReason)throw envelope();return this.#done=!0,!0}const value=JSON.parse(data);if(!record(value)||typeof value.id!="string"||!value.id||typeof value.model!="string"||!value.model||!Array.isArray(value.choices)||this.#id!==void 0&&this.#id!==value.id||this.#model!==void 0&&this.#model!==value.model||value.error!=null)throw envelope();this.#id=value.id,this.#model=value.model;const usage=acceptUsage(value.usage);if(usage&&this.#usage)throw envelope();if(value.choices.length===0){if(!this.#finishReason||!usage)throw envelope();return this.#usage=usage,this.#wireUsage=value.usage,!1}if(this.#finishReason||value.choices.length!==1)throw envelope();const choice=value.choices[0];if(!record(choice)||choice.index!==0||!record(choice.delta))throw envelope();const delta=choice.delta;if(delta.role!==void 0&&delta.role!=="assistant"||delta.function_call!=null||delta.refusal!=null||choice.message!=null||delta.reasoning_content!=null&&typeof delta.reasoning_content!="string"||delta.content!=null&&typeof delta.content!="string")throw envelope();const finishReason=choice.finish_reason;if(finishReason!=null&&(typeof finishReason!="string"||!finishReason)||usage&&!finishReason)throw envelope();if(typeof finishReason=="string"&&(this.#finishReason=finishReason),usage&&(this.#usage=usage,this.#wireUsage=value.usage),typeof delta.reasoning_content=="string"&&(this.#reasoning.push(delta.reasoning_content),this.#hasReasoning=!0),delta.tool_calls!=null){if(!Array.isArray(delta.tool_calls)||delta.tool_calls.length>toolLimit)throw envelope();for(const fragment of delta.tool_calls){if(!record(fragment)||typeof fragment.index!="number"||!Number.isSafeInteger(fragment.index)||fragment.index<0||fragment.index>=toolLimit||fragment.type!==void 0&&fragment.type!=="function"||fragment.id!==void 0&&(typeof fragment.id!="string"||!fragment.id)||fragment.function!==void 0&&!record(fragment.function))throw envelope();let call=this.#calls.get(fragment.index);if(call||(call={id:"",type:"function",function:{name:"",arguments:""}},this.#calls.set(fragment.index,call)),typeof fragment.id=="string"){if(call.id)throw envelope();call.id=fragment.id}if(record(fragment.function)){const{name,arguments:args}=fragment.function;if(name!==void 0&&typeof name!="string"||args!==void 0&&typeof args!="string")throw envelope();typeof name=="string"&&(call.function.name+=name),typeof args=="string"&&(call.function.arguments+=args)}}}if(typeof delta.content=="string"&&(this.#hasContent=!0),typeof delta.content=="string"&&delta.content.length){if(this.#fragments.push(delta.content),this.emit)try{const returned=this.emit(delta.content);if((typeof returned=="object"&&returned!==null||typeof returned=="function")&&"then"in returned&&typeof returned.then=="function")throw Promise.resolve(returned).catch(()=>{}),new TypeError("Model preview consumer must be synchronous")}catch(failure){throw new ModelEmissionFailure(failure)}this.signal.throwIfAborted()}return!1}result(){if(!this.#done||!this.#id||!this.#model||!this.#finishReason)throw envelope();const indexes=[...this.#calls.keys()].sort((left,right)=>left-right);if(indexes.some((index,position)=>index!==position))throw envelope();return acceptWireReply({id:this.#id,model:this.#model,choices:[{index:0,message:{role:"assistant",content:this.#hasContent||!this.#calls.size?this.#fragments.join(""):null,...this.#hasReasoning?{reasoning_content:this.#reasoning.join("")}:{},...this.#calls.size?{tool_calls:indexes.map(index=>this.#calls.get(index))}:{}},finish_reason:this.#finishReason}],usage:this.#wireUsage})}}function record(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}__name(record,"record");function envelope(){return new ProviderError("ENVELOPE","Model response is not a supported Chat Completions stream")}__name(envelope,"envelope");export{readModelStream};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export type JsonValue = null | boolean | number | string | readonly JsonValue[] | JsonObject;
|
|
2
|
+
export interface JsonObject {
|
|
3
|
+
readonly [key: string]: JsonValue;
|
|
4
|
+
}
|
|
5
|
+
export interface ModelMessage {
|
|
6
|
+
readonly role: 'system' | 'user' | 'assistant';
|
|
7
|
+
readonly content: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ModelTextOutput {
|
|
10
|
+
readonly kind: 'text';
|
|
11
|
+
readonly text: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ModelToolCall {
|
|
14
|
+
readonly kind: 'tool-call';
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly arguments: {
|
|
18
|
+
readonly encoding: 'json-text';
|
|
19
|
+
readonly text: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export type ModelOutput = ModelTextOutput | ModelToolCall;
|
|
23
|
+
export interface ModelUsage {
|
|
24
|
+
readonly promptTokens: number;
|
|
25
|
+
readonly completionTokens: number;
|
|
26
|
+
readonly totalTokens: number;
|
|
27
|
+
readonly promptCacheHitTokens?: number;
|
|
28
|
+
readonly promptCacheMissTokens?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ModelReply {
|
|
31
|
+
readonly formatVersion: 1;
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly model: string;
|
|
34
|
+
readonly output: readonly ModelOutput[];
|
|
35
|
+
readonly termination: {
|
|
36
|
+
readonly kind: 'completed' | 'incomplete' | 'refused';
|
|
37
|
+
readonly reason: string;
|
|
38
|
+
};
|
|
39
|
+
readonly usage: ModelUsage | null;
|
|
40
|
+
readonly native: {
|
|
41
|
+
readonly format: 'chat-completions';
|
|
42
|
+
readonly version: 1;
|
|
43
|
+
readonly reasoningContent?: string;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export interface ToolResult {
|
|
47
|
+
readonly status: 'ok' | 'error';
|
|
48
|
+
readonly content: readonly ModelTextOutput[];
|
|
49
|
+
}
|
|
50
|
+
export interface ToolFeedback extends ToolResult {
|
|
51
|
+
readonly toolCallId: string;
|
|
52
|
+
}
|
|
53
|
+
export interface ModelExchange {
|
|
54
|
+
readonly reply: ModelReply;
|
|
55
|
+
readonly feedback: readonly ToolFeedback[];
|
|
56
|
+
}
|
|
57
|
+
export interface ModelTool {
|
|
58
|
+
readonly name: string;
|
|
59
|
+
readonly description: string;
|
|
60
|
+
readonly parameters: JsonObject;
|
|
61
|
+
}
|
|
62
|
+
export type ModelToolChoice = 'auto' | 'none' | 'required' | {
|
|
63
|
+
readonly name: string;
|
|
64
|
+
};
|
|
65
|
+
export interface ModelRequest {
|
|
66
|
+
readonly messages: readonly ModelMessage[];
|
|
67
|
+
readonly exchanges?: readonly ModelExchange[];
|
|
68
|
+
readonly tools?: readonly ModelTool[];
|
|
69
|
+
readonly toolChoice?: ModelToolChoice;
|
|
70
|
+
readonly maxTokens?: number;
|
|
71
|
+
readonly responseFormat?: 'text' | 'json_object';
|
|
72
|
+
readonly temperature?: number;
|
|
73
|
+
readonly stream?: boolean;
|
|
74
|
+
}
|
|
75
|
+
export interface ModelControls {
|
|
76
|
+
readonly signal?: AbortSignal;
|
|
77
|
+
readonly emit?: (delta: string) => void;
|
|
78
|
+
}
|
|
79
|
+
export interface ModelProvider {
|
|
80
|
+
inspectRequest?(request: ModelRequest): ModelRequestInspection;
|
|
81
|
+
complete(request: ModelRequest, controls?: ModelControls): Promise<ModelReply>;
|
|
82
|
+
}
|
|
83
|
+
export interface ModelRequestInspection {
|
|
84
|
+
readonly requestBytes: number;
|
|
85
|
+
readonly wireBytes: number;
|
|
86
|
+
}
|
|
87
|
+
export interface ChatCompletionsConfiguration {
|
|
88
|
+
readonly endpoint: string;
|
|
89
|
+
readonly apiKey: string;
|
|
90
|
+
readonly model: string;
|
|
91
|
+
readonly timeoutMs?: number;
|
|
92
|
+
readonly maxResponseBytes?: number;
|
|
93
|
+
readonly thinking?: 'enabled' | 'disabled';
|
|
94
|
+
readonly fetch?: typeof fetch;
|
|
95
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
|
package/dist/usage.d.ts
ADDED
package/dist/usage.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
|
|
2
|
+
var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{ProviderError}from"./error.js";function acceptUsage(value){if(value==null)return null;if(typeof value!="object"||Array.isArray(value)||!("prompt_tokens"in value)||!("completion_tokens"in value)||!("total_tokens"in value)||!count(value.prompt_tokens)||!count(value.completion_tokens)||!count(value.total_tokens))throw invalidUsage();const hit="prompt_cache_hit_tokens"in value?value.prompt_cache_hit_tokens:void 0,miss="prompt_cache_miss_tokens"in value?value.prompt_cache_miss_tokens:void 0;if(hit!==void 0&&(!count(hit)||hit>value.prompt_tokens)||miss!==void 0&&(!count(miss)||miss>value.prompt_tokens)||typeof hit=="number"&&typeof miss=="number"&&hit!==value.prompt_tokens-miss)throw invalidUsage();return Object.freeze({promptTokens:value.prompt_tokens,completionTokens:value.completion_tokens,totalTokens:value.total_tokens,...hit===void 0?{}:{promptCacheHitTokens:hit},...miss===void 0?{}:{promptCacheMissTokens:miss}})}__name(acceptUsage,"acceptUsage");function count(value){return typeof value=="number"&&Number.isSafeInteger(value)&&value>=0}__name(count,"count");function invalidUsage(){return new ProviderError("ENVELOPE","Model response contains invalid usage counts")}__name(invalidUsage,"invalidUsage");export{acceptUsage};
|
package/dist/value.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const valueLimit: number;
|
|
2
|
+
export declare const toolLimit = 32;
|
|
3
|
+
export declare function requireJson(value: unknown): number;
|
|
4
|
+
export declare function freezeJson<T>(value: T): T;
|
|
5
|
+
export declare function record(value: unknown): value is Record<string, unknown>;
|
|
6
|
+
export declare function shape(value: unknown, required: readonly string[], optional?: readonly string[]): asserts value is Record<string, unknown>;
|
|
7
|
+
export declare function identity(value: unknown): asserts value is string;
|
|
8
|
+
export declare function count(value: unknown): value is number;
|
package/dist/value.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
|
|
2
|
+
var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{isProxy}from"node:util/types";const valueLimit=2*1024*1024,toolLimit=32;function requireJson(value){let nodes=0,bytes=0;const ancestors=new Set,add=__name(size=>{if(bytes+=size,bytes>valueLimit)throw new TypeError("Model value exceeds byte limit")},"add"),visit=__name((item,depth)=>{if(++nodes>1e5||depth>64)throw new TypeError("Model value exceeds structural limits");if(item===null||typeof item=="boolean"){add(item===null||item?4:5);return}if(typeof item=="string"){if(Buffer.byteLength(item)>valueLimit)throw new TypeError("Model value exceeds byte limit");add(Buffer.byteLength(JSON.stringify(item)));return}if(typeof item=="number"&&Number.isFinite(item)){add(Buffer.byteLength(JSON.stringify(item)));return}if(typeof item!="object"||isProxy(item)||ancestors.has(item))throw new TypeError("Model value must be acyclic JSON");const array=Array.isArray(item);if(!array&&Object.getPrototypeOf(item)!==Object.prototype&&Object.getPrototypeOf(item)!==null)throw new TypeError("Model value must contain plain objects");const descriptors=Object.getOwnPropertyDescriptors(item),keys=Reflect.ownKeys(descriptors);if(array&&keys.length!==item.length+1)throw new TypeError("Model arrays must be dense");const length=keys.length-(array?1:0);add(2+Math.max(0,length-1)),ancestors.add(item);for(const key of keys){if(array&&key==="length")continue;if(typeof key!="string")throw new TypeError("Model value cannot contain symbol fields");const descriptor=descriptors[key];if(!descriptor.enumerable||!("value"in descriptor))throw new TypeError("Model value cannot contain accessors or hidden fields");if(array&&(!/^(0|[1-9][0-9]*)$/u.test(key)||Number(key)>=item.length))throw new TypeError("Model arrays cannot have extra fields");array||add(Buffer.byteLength(JSON.stringify(key))+1),visit(descriptor.value,depth+1)}ancestors.delete(item)},"visit");return visit(value,0),bytes}__name(requireJson,"requireJson");function freezeJson(value){if(value!==null&&typeof value=="object"){for(const child of Object.values(value))freezeJson(child);Object.freeze(value)}return value}__name(freezeJson,"freezeJson");function record(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)}__name(record,"record");function shape(value,required,optional=[]){if(!record(value)||required.some(key=>!Object.hasOwn(value,key))||Object.keys(value).some(key=>!required.includes(key)&&!optional.includes(key)))throw new TypeError("Model value has unsupported or missing fields")}__name(shape,"shape");function identity(value){if(typeof value!="string"||value.length<1||value.length>256)throw new TypeError("Model identity is invalid")}__name(identity,"identity");function count(value){return typeof value=="number"&&Number.isSafeInteger(value)&&value>=0}__name(count,"count");export{count,freezeJson,identity,record,requireJson,shape,toolLimit,valueLimit};
|
package/dist/wire.d.ts
ADDED
package/dist/wire.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
|
|
2
|
+
var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{requireModelReply,terminationKind}from"./contract.js";import{ProviderError}from"./error.js";import{acceptUsage}from"./usage.js";import{freezeJson,record,toolLimit}from"./value.js";function acceptWireReply(value){if(!record(value)||!Array.isArray(value.choices)||value.choices.length!==1||value.error!=null)throw envelope();const choice=value.choices[0];if(!record(choice)||choice.index!==void 0&&choice.index!==0||!record(choice.message)||typeof choice.finish_reason!="string"||!choice.finish_reason)throw envelope();const message=choice.message;if(message.role!==void 0&&message.role!=="assistant"||message.function_call!=null||message.refusal!=null||message.content!==null&&typeof message.content!="string"||message.reasoning_content!=null&&typeof message.reasoning_content!="string")throw envelope();const output=typeof message.content=="string"?[{kind:"text",text:message.content}]:[];if(message.tool_calls!==void 0&&message.tool_calls!==null){if(!Array.isArray(message.tool_calls)||message.tool_calls.length>toolLimit)throw envelope();for(const call of message.tool_calls){if(!record(call)||typeof call.id!="string"||call.type!=="function"||!record(call.function)||typeof call.function.name!="string"||typeof call.function.arguments!="string")throw envelope();output.push({kind:"tool-call",id:call.id,name:call.function.name,arguments:{encoding:"json-text",text:call.function.arguments}})}}if(message.content===""&&output.some(item=>item.kind==="tool-call")&&output.shift(),message.content===null&&!output.length&&choice.finish_reason==="stop")throw envelope();const reply={formatVersion:1,id:value.id,model:value.model,output,termination:{kind:terminationKind(choice.finish_reason),reason:choice.finish_reason},usage:acceptUsage(value.usage),native:{format:"chat-completions",version:1,...typeof message.reasoning_content=="string"?{reasoningContent:message.reasoning_content}:{}}};try{return freezeJson(requireModelReply(reply))}catch{throw envelope()}}__name(acceptWireReply,"acceptWireReply");function encodeMessages(request){return[...request.messages.map(message=>({role:message.role,content:message.content})),...(request.exchanges??[]).flatMap(encodeExchange)]}__name(encodeMessages,"encodeMessages");function encodeExchange({reply,feedback}){const calls=reply.output.filter(item=>item.kind==="tool-call"),byId=new Map(feedback.map(item=>[item.toolCallId,item]));return[{role:"assistant",content:reply.output.find(item=>item.kind==="text")?.text??null,...reply.native.reasoningContent===void 0?{}:{reasoning_content:reply.native.reasoningContent},...calls.length?{tool_calls:calls.map(call=>({id:call.id,type:"function",function:{name:call.name,arguments:call.arguments.text}}))}:{}},...calls.map(call=>{const result=byId.get(call.id);return{role:"tool",tool_call_id:call.id,content:JSON.stringify({status:result.status,content:result.content})}})]}__name(encodeExchange,"encodeExchange");function envelope(){return new ProviderError("ENVELOPE","Model response is not a supported Chat Completions reply")}__name(envelope,"envelope");export{acceptWireReply,encodeMessages};
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weiliang0121/capix-model-provider",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"description": "Chat Completions model protocol adapter with streaming and native tool calls for CAPiX.",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "wei.liang (https://github.com/weiliang0121)",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/weiliang0121/capix.git",
|
|
18
|
+
"directory": "packages/model-provider"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22.23.2"
|
|
22
|
+
},
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"files": [
|
|
26
|
+
"dist/**/*.js",
|
|
27
|
+
"dist/**/*.d.ts",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public",
|
|
33
|
+
"registry": "https://registry.npmjs.org"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.json",
|
|
37
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
38
|
+
"test": "node ../../scripts/test/run.mjs --package ."
|
|
39
|
+
}
|
|
6
40
|
}
|