@qjp88995/provider-client 0.2.0 → 0.4.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/image/http.d.ts +16 -0
- package/dist/image/http.d.ts.map +1 -0
- package/dist/image/http.js +62 -0
- package/dist/image/http.js.map +1 -0
- package/dist/image/index.d.ts +32 -0
- package/dist/image/index.d.ts.map +1 -0
- package/dist/image/index.js +39 -0
- package/dist/image/index.js.map +1 -0
- package/dist/image/types.d.ts +94 -0
- package/dist/image/types.d.ts.map +1 -0
- package/dist/image/types.js +44 -0
- package/dist/image/types.js.map +1 -0
- package/dist/image/volcengine.adapter.d.ts +8 -0
- package/dist/image/volcengine.adapter.d.ts.map +1 -0
- package/dist/image/volcengine.adapter.js +85 -0
- package/dist/image/volcengine.adapter.js.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -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 +62 -0
- package/dist/provider-client.d.ts.map +1 -1
- package/dist/provider-client.js +176 -0
- package/dist/provider-client.js.map +1 -1
- package/dist/types.d.ts +23 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +12 -2
- 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/image/http.ts +68 -0
- package/src/image/index.ts +87 -0
- package/src/image/types.ts +134 -0
- package/src/image/volcengine.adapter.ts +133 -0
- package/src/index.ts +126 -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 +303 -1
- package/src/types.ts +31 -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,24 @@
|
|
|
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
|
+
createImageGenerator,
|
|
5
|
+
IMAGE_ADAPTERS,
|
|
6
|
+
type ImageGenerator,
|
|
7
|
+
isImageKind,
|
|
8
|
+
} from './image/index.js';
|
|
9
|
+
import {
|
|
10
|
+
createJustOne,
|
|
11
|
+
type JustOne,
|
|
12
|
+
type JustOneOptions,
|
|
13
|
+
} from './justone/index.js';
|
|
14
|
+
import {
|
|
15
|
+
createMedia,
|
|
16
|
+
isMediaKind,
|
|
17
|
+
MEDIA_ADAPTERS,
|
|
18
|
+
type MediaKind,
|
|
19
|
+
type MediaService,
|
|
20
|
+
type MediaSettings,
|
|
21
|
+
} from './media/index.js';
|
|
3
22
|
import {
|
|
4
23
|
buildHandle,
|
|
5
24
|
findModel,
|
|
@@ -10,6 +29,35 @@ import {
|
|
|
10
29
|
} from './model-handle.js';
|
|
11
30
|
import { type CatalogResponse, PC_HEADERS } from './types.js';
|
|
12
31
|
import type { VendorFetch } from './vendors/index.js';
|
|
32
|
+
import {
|
|
33
|
+
createWebSearch,
|
|
34
|
+
isWebSearchKind,
|
|
35
|
+
type WebSearch,
|
|
36
|
+
type WebSearchKind,
|
|
37
|
+
type WebSearchSettings,
|
|
38
|
+
} from './web-search/index.js';
|
|
39
|
+
|
|
40
|
+
/** `ProviderClient.webSearch` 的选项。 */
|
|
41
|
+
export interface WebSearchOptions<
|
|
42
|
+
K extends WebSearchKind = WebSearchKind,
|
|
43
|
+
> extends FetchForOptions {
|
|
44
|
+
/** 这家的设置(智谱档位、Kimi 基础 / 专业版);不给用缺省。 */
|
|
45
|
+
settings?: WebSearchSettings<K>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `ProviderClient.media` 的选项。 */
|
|
49
|
+
export interface MediaOptions<
|
|
50
|
+
K extends MediaKind = MediaKind,
|
|
51
|
+
> extends FetchForOptions {
|
|
52
|
+
/** 这家的设置(智谱的转写模型、解析档位与轮询预算);不给用缺省。 */
|
|
53
|
+
settings?: MediaSettings<K>;
|
|
54
|
+
/** 下载非厂商地址(`kind: 'url'` 的输入、智谱的结果包)用的 fetch;默认全局 fetch。不经中心。 */
|
|
55
|
+
download?: VendorFetch;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `ProviderClient.justOne` 的选项。 */
|
|
59
|
+
export interface JustOneClientOptions<C = unknown>
|
|
60
|
+
extends FetchForOptions, Pick<JustOneOptions<C>, 'onAttempt'> {}
|
|
13
61
|
|
|
14
62
|
/** `ProviderClient.fetchFor` 的选项。 */
|
|
15
63
|
export interface FetchForOptions {
|
|
@@ -21,6 +69,9 @@ export interface FetchForOptions {
|
|
|
21
69
|
onCredential?: (ref: string) => void;
|
|
22
70
|
}
|
|
23
71
|
|
|
72
|
+
/** `ProviderClient.imageModel` 的选项。 */
|
|
73
|
+
export type ImageModelOptions = FetchForOptions;
|
|
74
|
+
|
|
24
75
|
export interface ProviderClientOptions {
|
|
25
76
|
/** 中心的根地址,如 `https://provider.example.com`。 */
|
|
26
77
|
centerUrl: string;
|
|
@@ -133,6 +184,234 @@ export class ProviderClient implements ModelResolver {
|
|
|
133
184
|
});
|
|
134
185
|
}
|
|
135
186
|
|
|
187
|
+
/**
|
|
188
|
+
* 某家厂商的联网搜索 / 读网页(适配器在本包,请求经中心透传、中心注入 key)。地址取自目录里这家的
|
|
189
|
+
* `web-search` 端点——它可以与对话端点不同域(MiniMax)。
|
|
190
|
+
* - 目录里没有这家 → `unknown_vendor`;这个版本的 client 没有这家的搜索适配器 → `unsupported_vendor_kind`;
|
|
191
|
+
* 这家没有启用的 `web-search` 端点 → `protocol_unavailable`。
|
|
192
|
+
* - 调用时中心自己报的错(没有可用 key、不许用这家……)是 `ProviderClientError`,厂商的失败是
|
|
193
|
+
* `WebSearchUpstreamError`(带 `retryable`)。
|
|
194
|
+
* 目录只在创建时查一次。
|
|
195
|
+
*/
|
|
196
|
+
async webSearch<K extends WebSearchKind>(
|
|
197
|
+
slug: string,
|
|
198
|
+
options: WebSearchOptions<K> = {}
|
|
199
|
+
): Promise<WebSearch> {
|
|
200
|
+
const vendor = (await this.catalog()).vendors.find(v => v.slug === slug);
|
|
201
|
+
if (!vendor) {
|
|
202
|
+
throw new ProviderClientError(
|
|
203
|
+
'unknown_vendor',
|
|
204
|
+
`vendor "${slug}" is not in the catalog`,
|
|
205
|
+
null
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (!isWebSearchKind(vendor.kind)) {
|
|
209
|
+
throw new ProviderClientError(
|
|
210
|
+
'unsupported_vendor_kind',
|
|
211
|
+
`vendor kind "${vendor.kind}" has no web search adapter`,
|
|
212
|
+
null
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
const endpoint = vendor.endpoints.find(e => e.protocol === 'web-search');
|
|
216
|
+
if (!endpoint) {
|
|
217
|
+
throw new ProviderClientError(
|
|
218
|
+
'protocol_unavailable',
|
|
219
|
+
`vendor "${slug}" has no web-search endpoint`,
|
|
220
|
+
null
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
const fetch = createCenterFetch({
|
|
224
|
+
centerUrl: this.center,
|
|
225
|
+
apiKey: this.options.apiKey,
|
|
226
|
+
vendor: vendor.slug,
|
|
227
|
+
baseUrls: vendor.endpoints.map(e => e.baseUrl),
|
|
228
|
+
affinity: options.affinity,
|
|
229
|
+
pin: options.pin,
|
|
230
|
+
onCredential: options.onCredential,
|
|
231
|
+
fetch: this.send,
|
|
232
|
+
});
|
|
233
|
+
return createWebSearch({
|
|
234
|
+
kind: vendor.kind as K,
|
|
235
|
+
connection: { baseUrl: endpoint.baseUrl, fetch },
|
|
236
|
+
settings: options.settings,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 一个生图模型(适配器在本包,请求经中心透传、中心注入 key)。地址取自这家的对话端点(生图模型没有自己的协议,
|
|
242
|
+
* 用哪个端点由这家的生图适配器定)。**一次 `generate` 出一张**;要几张就并发几次。
|
|
243
|
+
* - 目录里没有这家 → `unknown_vendor`;这家没有这个生图模型(或停用了)→ `unknown_model`;
|
|
244
|
+
* 这个版本的 client 没有这家的生图适配器 → `unsupported_vendor_kind`;这家没有启用的对应端点 → `protocol_unavailable`。
|
|
245
|
+
* - 生成时中心自己报的错是 `ProviderClientError`,厂商的失败是 `ImageUpstreamError`(`retryable`、`contentFiltered`)。
|
|
246
|
+
* - 用量(张数、参考图数)在结果里,单价在目录里(`catalog()` 的 `imageModels[].pricing`),钱由 APP 自己算。
|
|
247
|
+
* 目录只在创建时查一次。
|
|
248
|
+
*/
|
|
249
|
+
async imageModel(
|
|
250
|
+
ref: ModelRef,
|
|
251
|
+
options: ImageModelOptions = {}
|
|
252
|
+
): Promise<ImageGenerator> {
|
|
253
|
+
const vendor = (await this.catalog()).vendors.find(
|
|
254
|
+
v => v.slug === ref.vendor
|
|
255
|
+
);
|
|
256
|
+
if (!vendor) {
|
|
257
|
+
throw new ProviderClientError(
|
|
258
|
+
'unknown_vendor',
|
|
259
|
+
`vendor "${ref.vendor}" is not in the catalog`,
|
|
260
|
+
null
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
const model = vendor.imageModels.find(m => m.modelId === ref.modelId);
|
|
264
|
+
if (!model) {
|
|
265
|
+
throw new ProviderClientError(
|
|
266
|
+
'unknown_model',
|
|
267
|
+
`image model "${ref.modelId}" is not in the catalog of vendor "${ref.vendor}"`,
|
|
268
|
+
null
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
if (!isImageKind(vendor.kind)) {
|
|
272
|
+
throw new ProviderClientError(
|
|
273
|
+
'unsupported_vendor_kind',
|
|
274
|
+
`vendor kind "${vendor.kind}" has no image generation adapter`,
|
|
275
|
+
null
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const protocol = IMAGE_ADAPTERS[vendor.kind].endpointProtocol;
|
|
279
|
+
const endpoint = vendor.endpoints.find(e => e.protocol === protocol);
|
|
280
|
+
if (!endpoint) {
|
|
281
|
+
throw new ProviderClientError(
|
|
282
|
+
'protocol_unavailable',
|
|
283
|
+
`vendor "${ref.vendor}" has no ${protocol} endpoint (image generation uses its base URL)`,
|
|
284
|
+
null
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
const fetch = createCenterFetch({
|
|
288
|
+
centerUrl: this.center,
|
|
289
|
+
apiKey: this.options.apiKey,
|
|
290
|
+
vendor: vendor.slug,
|
|
291
|
+
baseUrls: vendor.endpoints.map(e => e.baseUrl),
|
|
292
|
+
affinity: options.affinity,
|
|
293
|
+
pin: options.pin,
|
|
294
|
+
onCredential: options.onCredential,
|
|
295
|
+
fetch: this.send,
|
|
296
|
+
});
|
|
297
|
+
return createImageGenerator({
|
|
298
|
+
kind: vendor.kind,
|
|
299
|
+
modelId: model.modelId,
|
|
300
|
+
referenceImages: model.capabilities.includes('reference-image'),
|
|
301
|
+
connection: { baseUrl: endpoint.baseUrl, fetch },
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 某家厂商的媒体解析(适配器在本包,请求经中心透传、中心注入 key):智谱的文件解析 / 语音转写(地址取自这家的
|
|
307
|
+
* `media` 端点)、BibiGPT 的视频链接解析(它唯一的 `http` 端点)。
|
|
308
|
+
* - 目录里没有这家 → `unknown_vendor`;这个版本的 client 没有这家的媒体适配器 → `unsupported_vendor_kind`;
|
|
309
|
+
* 这家没有启用的对应端点 → `protocol_unavailable`。
|
|
310
|
+
* - **每次调用是一个任务**:第一条请求带 `affinity`(给了的话)选 key,之后的轮询 / 取结果钉在那把 key 上
|
|
311
|
+
* (`x-pc-pin`)——任务属于那个账号,换 key 就查不到;那把 key 中途不可用会报 `pinned_credential_unavailable`。
|
|
312
|
+
* 给了 `pin` 就从头钉住。
|
|
313
|
+
* - 中心自己报的错是 `ProviderClientError`,厂商的失败是 `MediaUpstreamError`。
|
|
314
|
+
* 目录只在创建时查一次。
|
|
315
|
+
*/
|
|
316
|
+
async media<K extends MediaKind>(
|
|
317
|
+
slug: string,
|
|
318
|
+
options: MediaOptions<K> = {}
|
|
319
|
+
): Promise<MediaService> {
|
|
320
|
+
const vendor = (await this.catalog()).vendors.find(v => v.slug === slug);
|
|
321
|
+
if (!vendor) {
|
|
322
|
+
throw new ProviderClientError(
|
|
323
|
+
'unknown_vendor',
|
|
324
|
+
`vendor "${slug}" is not in the catalog`,
|
|
325
|
+
null
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
if (!isMediaKind(vendor.kind)) {
|
|
329
|
+
throw new ProviderClientError(
|
|
330
|
+
'unsupported_vendor_kind',
|
|
331
|
+
`vendor kind "${vendor.kind}" has no media adapter`,
|
|
332
|
+
null
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
const protocol = MEDIA_ADAPTERS[vendor.kind].endpointProtocol;
|
|
336
|
+
const endpoint = vendor.endpoints.find(e => e.protocol === protocol);
|
|
337
|
+
if (!endpoint) {
|
|
338
|
+
throw new ProviderClientError(
|
|
339
|
+
'protocol_unavailable',
|
|
340
|
+
`vendor "${slug}" has no ${protocol} endpoint`,
|
|
341
|
+
null
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
const base: CenterFetchOptions = {
|
|
345
|
+
centerUrl: this.center,
|
|
346
|
+
apiKey: this.options.apiKey,
|
|
347
|
+
vendor: vendor.slug,
|
|
348
|
+
baseUrls: vendor.endpoints.map(e => e.baseUrl),
|
|
349
|
+
affinity: options.affinity,
|
|
350
|
+
fetch: this.send,
|
|
351
|
+
};
|
|
352
|
+
return createMedia({
|
|
353
|
+
kind: vendor.kind as K,
|
|
354
|
+
connection: {
|
|
355
|
+
baseUrl: endpoint.baseUrl,
|
|
356
|
+
openTask: () =>
|
|
357
|
+
createTaskFetch(base, options.pin, options.onCredential),
|
|
358
|
+
...(options.download ? { download: options.download } : {}),
|
|
359
|
+
},
|
|
360
|
+
settings: options.settings,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* JustOne 数据接口(客户端在本包,请求经中心透传;token 由中心注入 query 或表单字段,本进程不碰)。地址取自这家的
|
|
366
|
+
* `http` 端点。调用审计、花费由 APP 经 `onAttempt` 记。
|
|
367
|
+
* - 目录里没有这家 → `unknown_vendor`;这家不是 `justone` → `unsupported_vendor_kind`;没有启用的 `http` 端点 →
|
|
368
|
+
* `protocol_unavailable`。
|
|
369
|
+
* - 调用时中心自己报的错是 `ProviderClientError`,JustOne 的失败是 `JustOneUpstreamError`,参数不对是 `JustOneParamsError`。
|
|
370
|
+
* 接口是无状态的,`affinity` / `pin` 只影响选哪把 key。目录只在创建时查一次。
|
|
371
|
+
*/
|
|
372
|
+
async justOne<C = unknown>(
|
|
373
|
+
slug: string,
|
|
374
|
+
options: JustOneClientOptions<C> = {}
|
|
375
|
+
): Promise<JustOne<C>> {
|
|
376
|
+
const vendor = (await this.catalog()).vendors.find(v => v.slug === slug);
|
|
377
|
+
if (!vendor) {
|
|
378
|
+
throw new ProviderClientError(
|
|
379
|
+
'unknown_vendor',
|
|
380
|
+
`vendor "${slug}" is not in the catalog`,
|
|
381
|
+
null
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
if (vendor.kind !== 'justone') {
|
|
385
|
+
throw new ProviderClientError(
|
|
386
|
+
'unsupported_vendor_kind',
|
|
387
|
+
`vendor kind "${vendor.kind}" is not justone`,
|
|
388
|
+
null
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
const endpoint = vendor.endpoints.find(e => e.protocol === 'http');
|
|
392
|
+
if (!endpoint) {
|
|
393
|
+
throw new ProviderClientError(
|
|
394
|
+
'protocol_unavailable',
|
|
395
|
+
`vendor "${slug}" has no http endpoint`,
|
|
396
|
+
null
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
const fetch = createCenterFetch({
|
|
400
|
+
centerUrl: this.center,
|
|
401
|
+
apiKey: this.options.apiKey,
|
|
402
|
+
vendor: vendor.slug,
|
|
403
|
+
baseUrls: vendor.endpoints.map(e => e.baseUrl),
|
|
404
|
+
affinity: options.affinity,
|
|
405
|
+
pin: options.pin,
|
|
406
|
+
onCredential: options.onCredential,
|
|
407
|
+
fetch: this.send,
|
|
408
|
+
});
|
|
409
|
+
return createJustOne<C>({
|
|
410
|
+
connection: { baseUrl: endpoint.baseUrl, fetch },
|
|
411
|
+
...(options.onAttempt ? { onAttempt: options.onAttempt } : {}),
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
136
415
|
async capabilitiesOf(ref: ModelRef) {
|
|
137
416
|
const vendor = (await this.catalog()).vendors.find(
|
|
138
417
|
v => v.slug === ref.vendor
|
|
@@ -166,6 +445,8 @@ export class ProviderClient implements ModelResolver {
|
|
|
166
445
|
}
|
|
167
446
|
if (!res.ok) throw await errorFromResponse(res);
|
|
168
447
|
const body = (await res.json()) as CatalogResponse;
|
|
448
|
+
// 中心 2026-11 之前没有 `imageModels`:补成空数组,后面不用到处判空。
|
|
449
|
+
for (const v of body.vendors) v.imageModels ??= [];
|
|
169
450
|
this.cached = {
|
|
170
451
|
etag: res.headers.get('etag'),
|
|
171
452
|
body,
|
|
@@ -174,3 +455,24 @@ export class ProviderClient implements ModelResolver {
|
|
|
174
455
|
return body;
|
|
175
456
|
}
|
|
176
457
|
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* 一个任务用的中心模式 fetch:还不知道落在哪把 key 时按粘性选(或按调用方给的 pin),第一次看到 `x-pc-credential`
|
|
461
|
+
* 之后的请求都钉在那把上。
|
|
462
|
+
*/
|
|
463
|
+
function createTaskFetch(
|
|
464
|
+
base: CenterFetchOptions,
|
|
465
|
+
pin: string | undefined,
|
|
466
|
+
onCredential: ((ref: string) => void) | undefined
|
|
467
|
+
): VendorFetch {
|
|
468
|
+
let pinned = pin;
|
|
469
|
+
const report = (ref: string) => {
|
|
470
|
+
pinned ??= ref;
|
|
471
|
+
onCredential?.(ref);
|
|
472
|
+
};
|
|
473
|
+
return (input, init) =>
|
|
474
|
+
createCenterFetch({ ...base, pin: pinned, onCredential: report })(
|
|
475
|
+
input,
|
|
476
|
+
init
|
|
477
|
+
);
|
|
478
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -13,8 +13,16 @@ export const LLM_PROTOCOLS = [
|
|
|
13
13
|
] as const;
|
|
14
14
|
export type LlmProtocol = (typeof LLM_PROTOCOLS)[number];
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
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;
|
|
18
26
|
export type EndpointProtocol = (typeof ENDPOINT_PROTOCOLS)[number];
|
|
19
27
|
|
|
20
28
|
/** 厂商类别:只有 `llm` 有模型与适配器;其余的只经中心透传(适配器在各 APP)。 */
|
|
@@ -25,6 +33,10 @@ export type VendorCategory = (typeof VENDOR_CATEGORIES)[number];
|
|
|
25
33
|
export const LLM_CAPABILITIES = ['vision', 'video', 'reasoning'] as const;
|
|
26
34
|
export type LlmCapability = (typeof LLM_CAPABILITIES)[number];
|
|
27
35
|
|
|
36
|
+
/** 生图模型的能力。`reference-image` = 能带参考图(图生图 / 改图)。 */
|
|
37
|
+
export const IMAGE_CAPABILITIES = ['reference-image'] as const;
|
|
38
|
+
export type ImageCapability = (typeof IMAGE_CAPABILITIES)[number];
|
|
39
|
+
|
|
28
40
|
/** 厂商类型。`llm` 类别的每一种对应一个适配器(`VENDOR_ADAPTERS`);bibigpt / justone 没有。 */
|
|
29
41
|
export const VENDOR_KINDS = [
|
|
30
42
|
'openai',
|
|
@@ -83,7 +95,10 @@ export interface CatalogVendor {
|
|
|
83
95
|
/** 非 `llm` 的厂商没有模型,接入方只拿它的端点经中心透传。 */
|
|
84
96
|
category: VendorCategory;
|
|
85
97
|
endpoints: CatalogEndpoint[];
|
|
98
|
+
/** 对话模型。 */
|
|
86
99
|
models: CatalogModel[];
|
|
100
|
+
/** 生图模型(中心 2026-11 起有;读目录时缺了按空数组补上)。 */
|
|
101
|
+
imageModels: CatalogImageModel[];
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
export interface CatalogEndpoint {
|
|
@@ -103,6 +118,20 @@ export interface CatalogModel {
|
|
|
103
118
|
pricing: ModelPricing | null;
|
|
104
119
|
}
|
|
105
120
|
|
|
121
|
+
/** 生图的单价(每张,十进制字符串)。 */
|
|
122
|
+
export interface ImagePricing {
|
|
123
|
+
currency: string;
|
|
124
|
+
perImage: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface CatalogImageModel {
|
|
128
|
+
/** 厂商侧的模型名(请求体里的 `model`)。 */
|
|
129
|
+
modelId: string;
|
|
130
|
+
displayName: string;
|
|
131
|
+
capabilities: ImageCapability[];
|
|
132
|
+
pricing: ImagePricing | null;
|
|
133
|
+
}
|
|
134
|
+
|
|
106
135
|
/** 中心的错误码(响应体 `{ code, message }`)。 */
|
|
107
136
|
export const CENTER_ERROR_CODES = [
|
|
108
137
|
'unauthorized',
|
|
@@ -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
|
+
}
|