@qjp88995/provider-client 0.1.1 → 0.3.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/dist/direct-resolver.d.ts.map +1 -1
- package/dist/direct-resolver.js +2 -0
- package/dist/direct-resolver.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/justone/client.d.ts +101 -0
- package/dist/justone/client.d.ts.map +1 -0
- package/dist/justone/client.js +272 -0
- package/dist/justone/client.js.map +1 -0
- package/dist/justone/endpoint-spec.d.ts +49 -0
- package/dist/justone/endpoint-spec.d.ts.map +1 -0
- package/dist/justone/endpoint-spec.js +2 -0
- package/dist/justone/endpoint-spec.js.map +1 -0
- package/dist/justone/endpoints.generated.d.ts +7927 -0
- package/dist/justone/endpoints.generated.d.ts.map +1 -0
- package/dist/justone/endpoints.generated.js +8508 -0
- package/dist/justone/endpoints.generated.js.map +1 -0
- package/dist/justone/errors.d.ts +52 -0
- package/dist/justone/errors.d.ts.map +1 -0
- package/dist/justone/errors.js +73 -0
- package/dist/justone/errors.js.map +1 -0
- package/dist/justone/index.d.ts +8 -0
- package/dist/justone/index.d.ts.map +1 -0
- package/dist/justone/index.js +7 -0
- package/dist/justone/index.js.map +1 -0
- package/dist/justone/params.d.ts +17 -0
- package/dist/justone/params.d.ts.map +1 -0
- package/dist/justone/params.js +72 -0
- package/dist/justone/params.js.map +1 -0
- package/dist/justone/pricing.d.ts +6 -0
- package/dist/justone/pricing.d.ts.map +1 -0
- package/dist/justone/pricing.generated.d.ts +303 -0
- package/dist/justone/pricing.generated.d.ts.map +1 -0
- package/dist/justone/pricing.generated.js +305 -0
- package/dist/justone/pricing.generated.js.map +1 -0
- package/dist/justone/pricing.js +11 -0
- package/dist/justone/pricing.js.map +1 -0
- package/dist/media/bibigpt.adapter.d.ts +43 -0
- package/dist/media/bibigpt.adapter.d.ts.map +1 -0
- package/dist/media/bibigpt.adapter.js +302 -0
- package/dist/media/bibigpt.adapter.js.map +1 -0
- package/dist/media/http.d.ts +21 -0
- package/dist/media/http.d.ts.map +1 -0
- package/dist/media/http.js +39 -0
- package/dist/media/http.js.map +1 -0
- package/dist/media/index.d.ts +44 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/index.js +47 -0
- package/dist/media/index.js.map +1 -0
- package/dist/media/types.d.ts +207 -0
- package/dist/media/types.d.ts.map +1 -0
- package/dist/media/types.js +21 -0
- package/dist/media/types.js.map +1 -0
- package/dist/media/zhipu-parser-package.d.ts +37 -0
- package/dist/media/zhipu-parser-package.d.ts.map +1 -0
- package/dist/media/zhipu-parser-package.js +108 -0
- package/dist/media/zhipu-parser-package.js.map +1 -0
- package/dist/media/zhipu.adapter.d.ts +37 -0
- package/dist/media/zhipu.adapter.d.ts.map +1 -0
- package/dist/media/zhipu.adapter.js +361 -0
- package/dist/media/zhipu.adapter.js.map +1 -0
- package/dist/provider-client.d.ts +65 -0
- package/dist/provider-client.d.ts.map +1 -1
- package/dist/provider-client.js +151 -0
- package/dist/provider-client.js.map +1 -1
- package/dist/types.d.ts +15 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +16 -1
- package/dist/types.js.map +1 -1
- package/dist/web-search/deepseek.adapter.d.ts +19 -0
- package/dist/web-search/deepseek.adapter.d.ts.map +1 -0
- package/dist/web-search/deepseek.adapter.js +82 -0
- package/dist/web-search/deepseek.adapter.js.map +1 -0
- package/dist/web-search/http.d.ts +28 -0
- package/dist/web-search/http.d.ts.map +1 -0
- package/dist/web-search/http.js +95 -0
- package/dist/web-search/http.js.map +1 -0
- package/dist/web-search/index.d.ts +27 -0
- package/dist/web-search/index.d.ts.map +1 -0
- package/dist/web-search/index.js +36 -0
- package/dist/web-search/index.js.map +1 -0
- package/dist/web-search/minimax.adapter.d.ts +13 -0
- package/dist/web-search/minimax.adapter.d.ts.map +1 -0
- package/dist/web-search/minimax.adapter.js +82 -0
- package/dist/web-search/minimax.adapter.js.map +1 -0
- package/dist/web-search/moonshotai.adapter.d.ts +19 -0
- package/dist/web-search/moonshotai.adapter.d.ts.map +1 -0
- package/dist/web-search/moonshotai.adapter.js +151 -0
- package/dist/web-search/moonshotai.adapter.js.map +1 -0
- package/dist/web-search/types.d.ts +120 -0
- package/dist/web-search/types.d.ts.map +1 -0
- package/dist/web-search/types.js +60 -0
- package/dist/web-search/types.js.map +1 -0
- package/dist/web-search/zhipu.adapter.d.ts +14 -0
- package/dist/web-search/zhipu.adapter.d.ts.map +1 -0
- package/dist/web-search/zhipu.adapter.js +98 -0
- package/dist/web-search/zhipu.adapter.js.map +1 -0
- package/package.json +4 -3
- package/src/direct-resolver.ts +2 -0
- package/src/index.ts +104 -0
- package/src/justone/client.ts +462 -0
- package/src/justone/endpoint-spec.ts +66 -0
- package/src/justone/endpoints.generated.ts +9258 -0
- package/src/justone/errors.ts +97 -0
- package/src/justone/index.ts +34 -0
- package/src/justone/params.ts +91 -0
- package/src/justone/pricing.generated.ts +308 -0
- package/src/justone/pricing.ts +11 -0
- package/src/media/bibigpt.adapter.ts +372 -0
- package/src/media/http.ts +43 -0
- package/src/media/index.ts +114 -0
- package/src/media/types.ts +233 -0
- package/src/media/zhipu-parser-package.ts +149 -0
- package/src/media/zhipu.adapter.ts +510 -0
- package/src/provider-client.ts +268 -1
- package/src/types.ts +23 -2
- package/src/web-search/deepseek.adapter.ts +149 -0
- package/src/web-search/http.ts +108 -0
- package/src/web-search/index.ts +70 -0
- package/src/web-search/minimax.adapter.ts +148 -0
- package/src/web-search/moonshotai.adapter.ts +228 -0
- package/src/web-search/types.ts +172 -0
- package/src/web-search/zhipu.adapter.ts +184 -0
package/src/provider-client.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
import { createCenterFetch } from './center-fetch.js';
|
|
1
|
+
import { type CenterFetchOptions, createCenterFetch } from './center-fetch.js';
|
|
2
2
|
import { errorFromResponse, ProviderClientError } from './errors.js';
|
|
3
|
+
import {
|
|
4
|
+
createJustOne,
|
|
5
|
+
type JustOne,
|
|
6
|
+
type JustOneOptions,
|
|
7
|
+
} from './justone/index.js';
|
|
8
|
+
import {
|
|
9
|
+
createMedia,
|
|
10
|
+
isMediaKind,
|
|
11
|
+
MEDIA_ADAPTERS,
|
|
12
|
+
type MediaKind,
|
|
13
|
+
type MediaService,
|
|
14
|
+
type MediaSettings,
|
|
15
|
+
} from './media/index.js';
|
|
3
16
|
import {
|
|
4
17
|
buildHandle,
|
|
5
18
|
findModel,
|
|
@@ -10,6 +23,46 @@ import {
|
|
|
10
23
|
} from './model-handle.js';
|
|
11
24
|
import { type CatalogResponse, PC_HEADERS } from './types.js';
|
|
12
25
|
import type { VendorFetch } from './vendors/index.js';
|
|
26
|
+
import {
|
|
27
|
+
createWebSearch,
|
|
28
|
+
isWebSearchKind,
|
|
29
|
+
type WebSearch,
|
|
30
|
+
type WebSearchKind,
|
|
31
|
+
type WebSearchSettings,
|
|
32
|
+
} from './web-search/index.js';
|
|
33
|
+
|
|
34
|
+
/** `ProviderClient.webSearch` 的选项。 */
|
|
35
|
+
export interface WebSearchOptions<
|
|
36
|
+
K extends WebSearchKind = WebSearchKind,
|
|
37
|
+
> extends FetchForOptions {
|
|
38
|
+
/** 这家的设置(智谱档位、Kimi 基础 / 专业版);不给用缺省。 */
|
|
39
|
+
settings?: WebSearchSettings<K>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `ProviderClient.media` 的选项。 */
|
|
43
|
+
export interface MediaOptions<
|
|
44
|
+
K extends MediaKind = MediaKind,
|
|
45
|
+
> extends FetchForOptions {
|
|
46
|
+
/** 这家的设置(智谱的转写模型、解析档位与轮询预算);不给用缺省。 */
|
|
47
|
+
settings?: MediaSettings<K>;
|
|
48
|
+
/** 下载非厂商地址(`kind: 'url'` 的输入、智谱的结果包)用的 fetch;默认全局 fetch。不经中心。 */
|
|
49
|
+
download?: VendorFetch;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `ProviderClient.justOne` 的选项。 */
|
|
53
|
+
export interface JustOneClientOptions<C = unknown>
|
|
54
|
+
extends FetchForOptions,
|
|
55
|
+
Pick<JustOneOptions<C>, 'onAttempt'> {}
|
|
56
|
+
|
|
57
|
+
/** `ProviderClient.fetchFor` 的选项。 */
|
|
58
|
+
export interface FetchForOptions {
|
|
59
|
+
/** 会话粘性键(`x-pc-affinity`):建异步任务与轮询带同一个,就落在同一把 key 上(只要它还可用)。 */
|
|
60
|
+
affinity?: string;
|
|
61
|
+
/** 钉住某把 key(`x-pc-pin`):那把不可用就直接报错,不换。 */
|
|
62
|
+
pin?: string;
|
|
63
|
+
/** 每次响应回来时告诉调用方实际用的是哪把 key(`x-pc-credential`)。 */
|
|
64
|
+
onCredential?: (ref: string) => void;
|
|
65
|
+
}
|
|
13
66
|
|
|
14
67
|
export interface ProviderClientOptions {
|
|
15
68
|
/** 中心的根地址,如 `https://provider.example.com`。 */
|
|
@@ -93,6 +146,199 @@ export class ProviderClient implements ModelResolver {
|
|
|
93
146
|
});
|
|
94
147
|
}
|
|
95
148
|
|
|
149
|
+
/**
|
|
150
|
+
* 某家厂商的中心模式 fetch,交给 APP 自己的适配器(联网搜索、媒体解析、JustOne 这类不走模型句柄的调用):
|
|
151
|
+
* 适配器照常用厂商的地址发请求,这个 fetch 把它改写到中心透传。可改写的 origin 取自目录里这家的端点,
|
|
152
|
+
* 非 LLM 厂商同样可用;适配器该用哪个 baseUrl 见 `catalog()` 里这家的 `endpoints`。
|
|
153
|
+
* 目录只在创建时查一次;目录里没有这家(或这个接入方看不到)时报 `unknown_vendor`。
|
|
154
|
+
*/
|
|
155
|
+
async fetchFor(
|
|
156
|
+
slug: string,
|
|
157
|
+
options: FetchForOptions = {}
|
|
158
|
+
): Promise<VendorFetch> {
|
|
159
|
+
const vendor = (await this.catalog()).vendors.find(v => v.slug === slug);
|
|
160
|
+
if (!vendor) {
|
|
161
|
+
throw new ProviderClientError(
|
|
162
|
+
'unknown_vendor',
|
|
163
|
+
`vendor "${slug}" is not in the catalog`,
|
|
164
|
+
null
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return createCenterFetch({
|
|
168
|
+
centerUrl: this.center,
|
|
169
|
+
apiKey: this.options.apiKey,
|
|
170
|
+
vendor: vendor.slug,
|
|
171
|
+
baseUrls: vendor.endpoints.map(e => e.baseUrl),
|
|
172
|
+
affinity: options.affinity,
|
|
173
|
+
pin: options.pin,
|
|
174
|
+
onCredential: options.onCredential,
|
|
175
|
+
fetch: this.send,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 某家厂商的联网搜索 / 读网页(适配器在本包,请求经中心透传、中心注入 key)。地址取自目录里这家的
|
|
181
|
+
* `web-search` 端点——它可以与对话端点不同域(MiniMax)。
|
|
182
|
+
* - 目录里没有这家 → `unknown_vendor`;这个版本的 client 没有这家的搜索适配器 → `unsupported_vendor_kind`;
|
|
183
|
+
* 这家没有启用的 `web-search` 端点 → `protocol_unavailable`。
|
|
184
|
+
* - 调用时中心自己报的错(没有可用 key、不许用这家……)是 `ProviderClientError`,厂商的失败是
|
|
185
|
+
* `WebSearchUpstreamError`(带 `retryable`)。
|
|
186
|
+
* 目录只在创建时查一次。
|
|
187
|
+
*/
|
|
188
|
+
async webSearch<K extends WebSearchKind>(
|
|
189
|
+
slug: string,
|
|
190
|
+
options: WebSearchOptions<K> = {}
|
|
191
|
+
): Promise<WebSearch> {
|
|
192
|
+
const vendor = (await this.catalog()).vendors.find(v => v.slug === slug);
|
|
193
|
+
if (!vendor) {
|
|
194
|
+
throw new ProviderClientError(
|
|
195
|
+
'unknown_vendor',
|
|
196
|
+
`vendor "${slug}" is not in the catalog`,
|
|
197
|
+
null
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (!isWebSearchKind(vendor.kind)) {
|
|
201
|
+
throw new ProviderClientError(
|
|
202
|
+
'unsupported_vendor_kind',
|
|
203
|
+
`vendor kind "${vendor.kind}" has no web search adapter`,
|
|
204
|
+
null
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const endpoint = vendor.endpoints.find(e => e.protocol === 'web-search');
|
|
208
|
+
if (!endpoint) {
|
|
209
|
+
throw new ProviderClientError(
|
|
210
|
+
'protocol_unavailable',
|
|
211
|
+
`vendor "${slug}" has no web-search endpoint`,
|
|
212
|
+
null
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
const fetch = createCenterFetch({
|
|
216
|
+
centerUrl: this.center,
|
|
217
|
+
apiKey: this.options.apiKey,
|
|
218
|
+
vendor: vendor.slug,
|
|
219
|
+
baseUrls: vendor.endpoints.map(e => e.baseUrl),
|
|
220
|
+
affinity: options.affinity,
|
|
221
|
+
pin: options.pin,
|
|
222
|
+
onCredential: options.onCredential,
|
|
223
|
+
fetch: this.send,
|
|
224
|
+
});
|
|
225
|
+
return createWebSearch({
|
|
226
|
+
kind: vendor.kind as K,
|
|
227
|
+
connection: { baseUrl: endpoint.baseUrl, fetch },
|
|
228
|
+
settings: options.settings,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 某家厂商的媒体解析(适配器在本包,请求经中心透传、中心注入 key):智谱的文件解析 / 语音转写(地址取自这家的
|
|
234
|
+
* `media` 端点)、BibiGPT 的视频链接解析(它唯一的 `http` 端点)。
|
|
235
|
+
* - 目录里没有这家 → `unknown_vendor`;这个版本的 client 没有这家的媒体适配器 → `unsupported_vendor_kind`;
|
|
236
|
+
* 这家没有启用的对应端点 → `protocol_unavailable`。
|
|
237
|
+
* - **每次调用是一个任务**:第一条请求带 `affinity`(给了的话)选 key,之后的轮询 / 取结果钉在那把 key 上
|
|
238
|
+
* (`x-pc-pin`)——任务属于那个账号,换 key 就查不到;那把 key 中途不可用会报 `pinned_credential_unavailable`。
|
|
239
|
+
* 给了 `pin` 就从头钉住。
|
|
240
|
+
* - 中心自己报的错是 `ProviderClientError`,厂商的失败是 `MediaUpstreamError`。
|
|
241
|
+
* 目录只在创建时查一次。
|
|
242
|
+
*/
|
|
243
|
+
async media<K extends MediaKind>(
|
|
244
|
+
slug: string,
|
|
245
|
+
options: MediaOptions<K> = {}
|
|
246
|
+
): Promise<MediaService> {
|
|
247
|
+
const vendor = (await this.catalog()).vendors.find(v => v.slug === slug);
|
|
248
|
+
if (!vendor) {
|
|
249
|
+
throw new ProviderClientError(
|
|
250
|
+
'unknown_vendor',
|
|
251
|
+
`vendor "${slug}" is not in the catalog`,
|
|
252
|
+
null
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
if (!isMediaKind(vendor.kind)) {
|
|
256
|
+
throw new ProviderClientError(
|
|
257
|
+
'unsupported_vendor_kind',
|
|
258
|
+
`vendor kind "${vendor.kind}" has no media adapter`,
|
|
259
|
+
null
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
const protocol = MEDIA_ADAPTERS[vendor.kind].endpointProtocol;
|
|
263
|
+
const endpoint = vendor.endpoints.find(e => e.protocol === protocol);
|
|
264
|
+
if (!endpoint) {
|
|
265
|
+
throw new ProviderClientError(
|
|
266
|
+
'protocol_unavailable',
|
|
267
|
+
`vendor "${slug}" has no ${protocol} endpoint`,
|
|
268
|
+
null
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
const base: CenterFetchOptions = {
|
|
272
|
+
centerUrl: this.center,
|
|
273
|
+
apiKey: this.options.apiKey,
|
|
274
|
+
vendor: vendor.slug,
|
|
275
|
+
baseUrls: vendor.endpoints.map(e => e.baseUrl),
|
|
276
|
+
affinity: options.affinity,
|
|
277
|
+
fetch: this.send,
|
|
278
|
+
};
|
|
279
|
+
return createMedia({
|
|
280
|
+
kind: vendor.kind as K,
|
|
281
|
+
connection: {
|
|
282
|
+
baseUrl: endpoint.baseUrl,
|
|
283
|
+
openTask: () =>
|
|
284
|
+
createTaskFetch(base, options.pin, options.onCredential),
|
|
285
|
+
...(options.download ? { download: options.download } : {}),
|
|
286
|
+
},
|
|
287
|
+
settings: options.settings,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* JustOne 数据接口(客户端在本包,请求经中心透传;token 由中心注入 query 或表单字段,本进程不碰)。地址取自这家的
|
|
293
|
+
* `http` 端点。调用审计、花费由 APP 经 `onAttempt` 记。
|
|
294
|
+
* - 目录里没有这家 → `unknown_vendor`;这家不是 `justone` → `unsupported_vendor_kind`;没有启用的 `http` 端点 →
|
|
295
|
+
* `protocol_unavailable`。
|
|
296
|
+
* - 调用时中心自己报的错是 `ProviderClientError`,JustOne 的失败是 `JustOneUpstreamError`,参数不对是 `JustOneParamsError`。
|
|
297
|
+
* 接口是无状态的,`affinity` / `pin` 只影响选哪把 key。目录只在创建时查一次。
|
|
298
|
+
*/
|
|
299
|
+
async justOne<C = unknown>(
|
|
300
|
+
slug: string,
|
|
301
|
+
options: JustOneClientOptions<C> = {}
|
|
302
|
+
): Promise<JustOne<C>> {
|
|
303
|
+
const vendor = (await this.catalog()).vendors.find(v => v.slug === slug);
|
|
304
|
+
if (!vendor) {
|
|
305
|
+
throw new ProviderClientError(
|
|
306
|
+
'unknown_vendor',
|
|
307
|
+
`vendor "${slug}" is not in the catalog`,
|
|
308
|
+
null
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (vendor.kind !== 'justone') {
|
|
312
|
+
throw new ProviderClientError(
|
|
313
|
+
'unsupported_vendor_kind',
|
|
314
|
+
`vendor kind "${vendor.kind}" is not justone`,
|
|
315
|
+
null
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
const endpoint = vendor.endpoints.find(e => e.protocol === 'http');
|
|
319
|
+
if (!endpoint) {
|
|
320
|
+
throw new ProviderClientError(
|
|
321
|
+
'protocol_unavailable',
|
|
322
|
+
`vendor "${slug}" has no http endpoint`,
|
|
323
|
+
null
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
const fetch = createCenterFetch({
|
|
327
|
+
centerUrl: this.center,
|
|
328
|
+
apiKey: this.options.apiKey,
|
|
329
|
+
vendor: vendor.slug,
|
|
330
|
+
baseUrls: vendor.endpoints.map(e => e.baseUrl),
|
|
331
|
+
affinity: options.affinity,
|
|
332
|
+
pin: options.pin,
|
|
333
|
+
onCredential: options.onCredential,
|
|
334
|
+
fetch: this.send,
|
|
335
|
+
});
|
|
336
|
+
return createJustOne<C>({
|
|
337
|
+
connection: { baseUrl: endpoint.baseUrl, fetch },
|
|
338
|
+
...(options.onAttempt ? { onAttempt: options.onAttempt } : {}),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
96
342
|
async capabilitiesOf(ref: ModelRef) {
|
|
97
343
|
const vendor = (await this.catalog()).vendors.find(
|
|
98
344
|
v => v.slug === ref.vendor
|
|
@@ -134,3 +380,24 @@ export class ProviderClient implements ModelResolver {
|
|
|
134
380
|
return body;
|
|
135
381
|
}
|
|
136
382
|
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* 一个任务用的中心模式 fetch:还不知道落在哪把 key 时按粘性选(或按调用方给的 pin),第一次看到 `x-pc-credential`
|
|
386
|
+
* 之后的请求都钉在那把上。
|
|
387
|
+
*/
|
|
388
|
+
function createTaskFetch(
|
|
389
|
+
base: CenterFetchOptions,
|
|
390
|
+
pin: string | undefined,
|
|
391
|
+
onCredential: ((ref: string) => void) | undefined
|
|
392
|
+
): VendorFetch {
|
|
393
|
+
let pinned = pin;
|
|
394
|
+
const report = (ref: string) => {
|
|
395
|
+
pinned ??= ref;
|
|
396
|
+
onCredential?.(ref);
|
|
397
|
+
};
|
|
398
|
+
return (input, init) =>
|
|
399
|
+
createCenterFetch({ ...base, pin: pinned, onCredential: report })(
|
|
400
|
+
input,
|
|
401
|
+
init
|
|
402
|
+
);
|
|
403
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -13,11 +13,27 @@ export const LLM_PROTOCOLS = [
|
|
|
13
13
|
] as const;
|
|
14
14
|
export type LlmProtocol = (typeof LLM_PROTOCOLS)[number];
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* 端点协议:LLM 线协议;非 LLM 厂商(媒体、数据接口)唯一的 `http`;LLM 厂商的联网搜索 / 读网页 `web-search`
|
|
18
|
+
* (可以与对话端点不同域,如 MiniMax);LLM 厂商借同一把 key 的媒体解析 `media`(智谱)。模型只声明 LLM 协议。
|
|
19
|
+
*/
|
|
20
|
+
export const ENDPOINT_PROTOCOLS = [
|
|
21
|
+
...LLM_PROTOCOLS,
|
|
22
|
+
'http',
|
|
23
|
+
'web-search',
|
|
24
|
+
'media',
|
|
25
|
+
] as const;
|
|
26
|
+
export type EndpointProtocol = (typeof ENDPOINT_PROTOCOLS)[number];
|
|
27
|
+
|
|
28
|
+
/** 厂商类别:只有 `llm` 有模型与适配器;其余的只经中心透传(适配器在各 APP)。 */
|
|
29
|
+
export const VENDOR_CATEGORIES = ['llm', 'media', 'data'] as const;
|
|
30
|
+
export type VendorCategory = (typeof VENDOR_CATEGORIES)[number];
|
|
31
|
+
|
|
16
32
|
/** 模型能力(目录元数据:发不发图片 / 开不开推理由调用方据此决定)。 */
|
|
17
33
|
export const LLM_CAPABILITIES = ['vision', 'video', 'reasoning'] as const;
|
|
18
34
|
export type LlmCapability = (typeof LLM_CAPABILITIES)[number];
|
|
19
35
|
|
|
20
|
-
/**
|
|
36
|
+
/** 厂商类型。`llm` 类别的每一种对应一个适配器(`VENDOR_ADAPTERS`);bibigpt / justone 没有。 */
|
|
21
37
|
export const VENDOR_KINDS = [
|
|
22
38
|
'openai',
|
|
23
39
|
'generic',
|
|
@@ -29,6 +45,8 @@ export const VENDOR_KINDS = [
|
|
|
29
45
|
'minimax',
|
|
30
46
|
'dots',
|
|
31
47
|
'google',
|
|
48
|
+
'bibigpt',
|
|
49
|
+
'justone',
|
|
32
50
|
] as const;
|
|
33
51
|
export type VendorKind = (typeof VENDOR_KINDS)[number];
|
|
34
52
|
|
|
@@ -70,12 +88,14 @@ export interface CatalogVendor {
|
|
|
70
88
|
slug: string;
|
|
71
89
|
name: string;
|
|
72
90
|
kind: VendorKind;
|
|
91
|
+
/** 非 `llm` 的厂商没有模型,接入方只拿它的端点经中心透传。 */
|
|
92
|
+
category: VendorCategory;
|
|
73
93
|
endpoints: CatalogEndpoint[];
|
|
74
94
|
models: CatalogModel[];
|
|
75
95
|
}
|
|
76
96
|
|
|
77
97
|
export interface CatalogEndpoint {
|
|
78
|
-
protocol:
|
|
98
|
+
protocol: EndpointProtocol;
|
|
79
99
|
baseUrl: string;
|
|
80
100
|
}
|
|
81
101
|
|
|
@@ -105,6 +125,7 @@ export const CENTER_ERROR_CODES = [
|
|
|
105
125
|
'vendor_not_allowed',
|
|
106
126
|
'no_credential',
|
|
107
127
|
'pinned_credential_unavailable',
|
|
128
|
+
'payload_too_large',
|
|
108
129
|
] as const;
|
|
109
130
|
export type CenterErrorCode = (typeof CENTER_ERROR_CODES)[number];
|
|
110
131
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { clip, joinUrl, parseErrorBody, parseJson, postJson } from './http.js';
|
|
2
|
+
import {
|
|
3
|
+
recencySince,
|
|
4
|
+
type WebPageOutcome,
|
|
5
|
+
type WebSearchAdapter,
|
|
6
|
+
type WebSearchConnection,
|
|
7
|
+
type WebSearchInput,
|
|
8
|
+
type WebSearchOutcome,
|
|
9
|
+
type WebSearchSettings,
|
|
10
|
+
type WebSearchTraits,
|
|
11
|
+
WebSearchUpstreamError,
|
|
12
|
+
} from './types.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* DeepSeek Search / Fetch API(2026-10-17 上线,<https://api-docs.deepseek.com/guides/search>)。
|
|
16
|
+
* 与对话 API 同一把 Bearer key;`web-search` 端点的根是 `https://api.deepseek.com/v1/tools`,下挂 `/search`、`/fetch`。
|
|
17
|
+
*/
|
|
18
|
+
const TRAITS: WebSearchTraits = {
|
|
19
|
+
readsPages: true,
|
|
20
|
+
maxResults: null,
|
|
21
|
+
filters: ['recency', 'domain'],
|
|
22
|
+
linksOftenMissing: false,
|
|
23
|
+
pageImagesArePlaceholders: false,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
interface DeepseekSearchBody {
|
|
27
|
+
results?: {
|
|
28
|
+
title?: string;
|
|
29
|
+
url?: string;
|
|
30
|
+
snippet?: string;
|
|
31
|
+
summary?: string;
|
|
32
|
+
site_name?: string | null;
|
|
33
|
+
date?: string | null;
|
|
34
|
+
}[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface DeepseekFetchBody {
|
|
38
|
+
title?: string;
|
|
39
|
+
content?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type Settings = WebSearchSettings<'deepseek'>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* DeepSeek 没有单独的时间 / 域名过滤字段,靠查询里的操作符(`after:` / `site:`),
|
|
46
|
+
* 所以这两项拼进 `query`。查询上限 128 字符(含操作符),工具层的 70 字留足了余量。
|
|
47
|
+
*/
|
|
48
|
+
export class DeepseekWebSearchAdapter implements WebSearchAdapter<'deepseek'> {
|
|
49
|
+
readonly kind = 'deepseek' as const;
|
|
50
|
+
|
|
51
|
+
constructor(private readonly now: () => Date = () => new Date()) {}
|
|
52
|
+
|
|
53
|
+
traits(): WebSearchTraits {
|
|
54
|
+
return TRAITS;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async search(
|
|
58
|
+
conn: WebSearchConnection,
|
|
59
|
+
_settings: Settings,
|
|
60
|
+
input: WebSearchInput,
|
|
61
|
+
signal: AbortSignal
|
|
62
|
+
): Promise<WebSearchOutcome> {
|
|
63
|
+
const query = [
|
|
64
|
+
input.query,
|
|
65
|
+
input.domain ? `site:${input.domain}` : null,
|
|
66
|
+
input.recency ? `after:${recencySince(input.recency, this.now())}` : null,
|
|
67
|
+
]
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.join(' ');
|
|
70
|
+
const data = await this.post<DeepseekSearchBody>(
|
|
71
|
+
'search',
|
|
72
|
+
conn,
|
|
73
|
+
{ query, ...(input.count !== undefined ? { count: input.count } : {}) },
|
|
74
|
+
signal
|
|
75
|
+
);
|
|
76
|
+
const items = (data.results ?? []).map(r => ({
|
|
77
|
+
title: r.title ?? '',
|
|
78
|
+
link: r.url ?? '',
|
|
79
|
+
// summary 是给模型看的长摘要(600–800 字),snippet 是给界面的短摘要。
|
|
80
|
+
content: r.summary || r.snippet || '',
|
|
81
|
+
media: r.site_name || null,
|
|
82
|
+
publishDate: r.date || null,
|
|
83
|
+
}));
|
|
84
|
+
return { engine: 'deepseek', items };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async readPage(
|
|
88
|
+
conn: WebSearchConnection,
|
|
89
|
+
_settings: Settings,
|
|
90
|
+
input: { url: string },
|
|
91
|
+
signal: AbortSignal
|
|
92
|
+
): Promise<WebPageOutcome> {
|
|
93
|
+
const data = await this.post<DeepseekFetchBody>(
|
|
94
|
+
'fetch',
|
|
95
|
+
conn,
|
|
96
|
+
{ url: input.url },
|
|
97
|
+
signal
|
|
98
|
+
);
|
|
99
|
+
if (typeof data.content !== 'string') {
|
|
100
|
+
throw new WebSearchUpstreamError('deepseek fetch 响应缺少 content');
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
url: input.url,
|
|
104
|
+
title: data.title || null,
|
|
105
|
+
description: null,
|
|
106
|
+
content: data.content,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private async post<T>(
|
|
111
|
+
path: 'search' | 'fetch',
|
|
112
|
+
conn: WebSearchConnection,
|
|
113
|
+
body: Record<string, unknown>,
|
|
114
|
+
signal: AbortSignal
|
|
115
|
+
): Promise<T> {
|
|
116
|
+
const label = `deepseek ${path}`;
|
|
117
|
+
const res = await postJson(
|
|
118
|
+
label,
|
|
119
|
+
joinUrl(conn.baseUrl, path),
|
|
120
|
+
conn,
|
|
121
|
+
body,
|
|
122
|
+
signal
|
|
123
|
+
);
|
|
124
|
+
if (!res.ok) {
|
|
125
|
+
const parsed = parseErrorBody(res.text);
|
|
126
|
+
const detail = parsed.message ?? clip(res.text);
|
|
127
|
+
const hint = STATUS_HINTS[res.status];
|
|
128
|
+
throw new WebSearchUpstreamError(
|
|
129
|
+
`${label} ${res.status}${hint ? `(${hint})` : ''}${detail ? `: ${detail}` : ''}`,
|
|
130
|
+
{
|
|
131
|
+
code: parsed.code,
|
|
132
|
+
// 文档:408 / 429 / 5xx 等一等再试,其余 4xx 不改请求重试也没用。
|
|
133
|
+
retryable:
|
|
134
|
+
res.status >= 500 || res.status === 429 || res.status === 408,
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return parseJson<T>(label, res.text);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 文档里有确定含义的状态码,带进错误文案让模型 / 运营不必去查。 */
|
|
143
|
+
const STATUS_HINTS: Readonly<Record<number, string>> = {
|
|
144
|
+
401: 'API key 无效',
|
|
145
|
+
402: '账户余额不足,需要充值',
|
|
146
|
+
403: '网页被 DeepSeek 的安全策略拒绝',
|
|
147
|
+
404: '取不到这个网页的可用内容:链接失效、站点拦截或不是可提取的网页',
|
|
148
|
+
429: '超过 QPS 上限',
|
|
149
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { parseCenterError, ProviderClientError } from '../errors.js';
|
|
2
|
+
import { PC_HEADERS } from '../types.js';
|
|
3
|
+
import { type WebSearchConnection, WebSearchUpstreamError } from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* POST 一段 JSON,返回状态码与响应正文。只处理「请求没发出去」与「中心自己报的错」:
|
|
7
|
+
* - 中止原样抛(那是调用方超时或用户点了停止,不是上游故障,重试是错的);网络错误转成可重试的上游错误。
|
|
8
|
+
* - 中心自己的错误体(`{ code, message }` 且没有 `x-pc-credential` 头,判据同 `toProviderClientError`):
|
|
9
|
+
* `upstream_error`(中心连不上厂商)当可重试的上游错误,其余(没有可用 key、不许用这家……)是配置问题,
|
|
10
|
+
* 抛 `ProviderClientError`。
|
|
11
|
+
* 厂商的状态码与响应体怎么解释是各适配器的事。
|
|
12
|
+
*/
|
|
13
|
+
export async function postJson(
|
|
14
|
+
label: string,
|
|
15
|
+
url: string,
|
|
16
|
+
{ apiKey, fetch }: WebSearchConnection,
|
|
17
|
+
body: Record<string, unknown>,
|
|
18
|
+
signal: AbortSignal
|
|
19
|
+
): Promise<{ status: number; ok: boolean; text: string }> {
|
|
20
|
+
let res: Response;
|
|
21
|
+
try {
|
|
22
|
+
res = await fetch(url, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: {
|
|
25
|
+
...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
|
|
26
|
+
'content-type': 'application/json',
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify(body),
|
|
29
|
+
signal,
|
|
30
|
+
});
|
|
31
|
+
} catch (err) {
|
|
32
|
+
if (signal.aborted) throw err;
|
|
33
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
34
|
+
throw new WebSearchUpstreamError(`${label} 请求失败:${reason}`, {
|
|
35
|
+
retryable: true,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const text = await res.text();
|
|
39
|
+
if (!res.ok && !res.headers.has(PC_HEADERS.credential)) {
|
|
40
|
+
const center = centerErrorOf(text);
|
|
41
|
+
if (center?.code === 'upstream_error') {
|
|
42
|
+
throw new WebSearchUpstreamError(`${label} 请求失败:${center.message}`, {
|
|
43
|
+
code: center.code,
|
|
44
|
+
retryable: true,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (center) {
|
|
48
|
+
throw new ProviderClientError(center.code, center.message, res.status);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { status: res.status, ok: res.ok, text };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function centerErrorOf(text: string) {
|
|
55
|
+
try {
|
|
56
|
+
return parseCenterError(JSON.parse(text));
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 解析 JSON 响应体;不成形就是上游失败。 */
|
|
63
|
+
export function parseJson<T>(label: string, text: string): T {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(text) as T;
|
|
66
|
+
} catch {
|
|
67
|
+
throw new WebSearchUpstreamError(`${label} 返回了无法解析的响应体`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** OpenAI 形状的错误体 `{error: {code, message}}`;其余形状当没有。 */
|
|
72
|
+
export function parseErrorBody(text: string): {
|
|
73
|
+
code: string | undefined;
|
|
74
|
+
message: string | undefined;
|
|
75
|
+
} {
|
|
76
|
+
try {
|
|
77
|
+
const body = JSON.parse(text) as {
|
|
78
|
+
error?: { code?: unknown; message?: unknown };
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
code: codeOf(body.error?.code),
|
|
82
|
+
message:
|
|
83
|
+
typeof body.error?.message === 'string'
|
|
84
|
+
? body.error.message
|
|
85
|
+
: undefined,
|
|
86
|
+
};
|
|
87
|
+
} catch {
|
|
88
|
+
return { code: undefined, message: undefined };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 错误码有时是字符串有时是数字,统一成字符串;其余形状当没有。 */
|
|
93
|
+
export function codeOf(value: unknown): string | undefined {
|
|
94
|
+
return typeof value === 'string' || typeof value === 'number'
|
|
95
|
+
? String(value)
|
|
96
|
+
: undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 错误详情别整段回灌(HTML 错误页可能几十 KB)。 */
|
|
100
|
+
export function clip(text: string): string {
|
|
101
|
+
const flat = text.replace(/\s+/g, ' ').trim();
|
|
102
|
+
return flat.length > 300 ? `${flat.slice(0, 300)}…` : flat;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 端点根 + 子路径(根上多余的 `/` 去掉)。 */
|
|
106
|
+
export function joinUrl(baseUrl: string, path: string): string {
|
|
107
|
+
return `${baseUrl.replace(/\/+$/, '')}/${path}`;
|
|
108
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { DeepseekWebSearchAdapter } from './deepseek.adapter.js';
|
|
2
|
+
import { MinimaxWebSearchAdapter } from './minimax.adapter.js';
|
|
3
|
+
import { MoonshotaiWebSearchAdapter } from './moonshotai.adapter.js';
|
|
4
|
+
import type {
|
|
5
|
+
WebPageOutcome,
|
|
6
|
+
WebSearchAdapter,
|
|
7
|
+
WebSearchConnection,
|
|
8
|
+
WebSearchInput,
|
|
9
|
+
WebSearchKind,
|
|
10
|
+
WebSearchOutcome,
|
|
11
|
+
WebSearchSettings,
|
|
12
|
+
WebSearchTraits,
|
|
13
|
+
} from './types.js';
|
|
14
|
+
import { ZhipuWebSearchAdapter } from './zhipu.adapter.js';
|
|
15
|
+
|
|
16
|
+
/** kind → 适配器。映射类型保证每个 kind 都有、且设置类型对得上;加一家漏了这里编译报错。 */
|
|
17
|
+
export const WEB_SEARCH_ADAPTERS: {
|
|
18
|
+
readonly [K in WebSearchKind]: WebSearchAdapter<K>;
|
|
19
|
+
} = {
|
|
20
|
+
deepseek: new DeepseekWebSearchAdapter(),
|
|
21
|
+
moonshotai: new MoonshotaiWebSearchAdapter(),
|
|
22
|
+
minimax: new MinimaxWebSearchAdapter(),
|
|
23
|
+
zhipu: new ZhipuWebSearchAdapter(),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function isWebSearchKind(kind: string): kind is WebSearchKind {
|
|
27
|
+
return Object.hasOwn(WEB_SEARCH_ADAPTERS, kind);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 一家联网服务,连接与设置都已绑好。`readPage` 只在 `traits.readsPages` 时存在。 */
|
|
31
|
+
export interface WebSearch {
|
|
32
|
+
readonly kind: WebSearchKind;
|
|
33
|
+
readonly traits: WebSearchTraits;
|
|
34
|
+
search(input: WebSearchInput, signal: AbortSignal): Promise<WebSearchOutcome>;
|
|
35
|
+
readPage?(
|
|
36
|
+
input: { url: string },
|
|
37
|
+
signal: AbortSignal
|
|
38
|
+
): Promise<WebPageOutcome>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CreateWebSearchInput<K extends WebSearchKind> {
|
|
42
|
+
kind: K;
|
|
43
|
+
connection: WebSearchConnection;
|
|
44
|
+
settings?: WebSearchSettings<K>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 绑好连接与设置的联网服务。中心模式用 `ProviderClient.webSearch`;这里是直连(本地开发、测试)与中心模式共用的底层:
|
|
49
|
+
* 直连时 `connection` 给厂商的 `web-search` 根地址 + `apiKey`。
|
|
50
|
+
*/
|
|
51
|
+
export function createWebSearch<K extends WebSearchKind>(
|
|
52
|
+
input: CreateWebSearchInput<K>
|
|
53
|
+
): WebSearch {
|
|
54
|
+
// 映射类型按 K 取出来是联合,TS 收窄不了泛型索引;kind 与 settings 由签名绑在一起。
|
|
55
|
+
const adapter = WEB_SEARCH_ADAPTERS[input.kind] as WebSearchAdapter<K>;
|
|
56
|
+
const settings = (input.settings ?? {}) as WebSearchSettings<K>;
|
|
57
|
+
const conn = input.connection;
|
|
58
|
+
const traits = adapter.traits(settings);
|
|
59
|
+
const readPage = adapter.readPage?.bind(adapter);
|
|
60
|
+
return {
|
|
61
|
+
kind: input.kind,
|
|
62
|
+
traits,
|
|
63
|
+
search: (q, signal) => adapter.search(conn, settings, q, signal),
|
|
64
|
+
...(readPage
|
|
65
|
+
? { readPage: (q, signal) => readPage(conn, settings, q, signal) }
|
|
66
|
+
: {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export * from './types.js';
|