@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
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { clip, codeOf, joinUrl, parseErrorBody, parseJson, postJson, } from './http.js';
|
|
2
|
+
import { WebSearchUpstreamError, } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* 智谱工具 API 的根取自这家的 `web-search` 端点(`https://open.bigmodel.cn/api/paas/v4`),下挂 `/web_search`、`/reader`。
|
|
5
|
+
* 不从对话端点推:chat 根是 `/api/paas/v4`、responses 根是 `/api/v1`,从哪条推都是猜。
|
|
6
|
+
*/
|
|
7
|
+
/** 不设档位时用高阶版(agent-base 的缺省)。 */
|
|
8
|
+
const DEFAULT_ENGINE = 'search_pro';
|
|
9
|
+
/** reader 让智谱侧抓取页面的预算(秒)——上游参数,不是本地超时。 */
|
|
10
|
+
const READER_FETCH_TIMEOUT_SECONDS = 20;
|
|
11
|
+
/** 智谱「工具 API」:联网搜索(web_search)+ 网页阅读(reader)。 */
|
|
12
|
+
export class ZhipuWebSearchAdapter {
|
|
13
|
+
kind = 'zhipu';
|
|
14
|
+
traits(settings) {
|
|
15
|
+
return {
|
|
16
|
+
readsPages: true,
|
|
17
|
+
maxResults: null,
|
|
18
|
+
filters: ['recency', 'domain'],
|
|
19
|
+
// 真机探针(2026-08-31):search_std 常返回空 link,「把链接交给读网页」就走不通。
|
|
20
|
+
linksOftenMissing: (settings.searchEngine ?? DEFAULT_ENGINE) === 'search_std',
|
|
21
|
+
pageImagesArePlaceholders: false,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async search(conn, settings, input, signal) {
|
|
25
|
+
const engine = settings.searchEngine ?? DEFAULT_ENGINE;
|
|
26
|
+
const body = {
|
|
27
|
+
search_query: input.query,
|
|
28
|
+
search_engine: engine,
|
|
29
|
+
// 意图识别用不上:调用方(模型)自己就是意图层,开了只多花一份钱。
|
|
30
|
+
search_intent: false,
|
|
31
|
+
...(input.count !== undefined ? { count: input.count } : {}),
|
|
32
|
+
...(input.recency ? { search_recency_filter: input.recency } : {}),
|
|
33
|
+
...(input.domain ? { search_domain_filter: input.domain } : {}),
|
|
34
|
+
};
|
|
35
|
+
let data;
|
|
36
|
+
try {
|
|
37
|
+
data = await this.post('web_search', conn, body, signal);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
// 1703 = 搜索引擎没返回有效数据。这是「零结果」不是「上游坏了」——报成
|
|
41
|
+
// 错误会让模型把一次正常的空搜索当成故障去重试,而它该做的是换关键词。
|
|
42
|
+
if (err instanceof WebSearchUpstreamError && err.code === '1703') {
|
|
43
|
+
return { engine, items: [] };
|
|
44
|
+
}
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
const items = (data.search_result ?? []).map(r => ({
|
|
48
|
+
title: r.title ?? '',
|
|
49
|
+
link: r.link ?? '',
|
|
50
|
+
content: r.content ?? '',
|
|
51
|
+
media: r.media || null,
|
|
52
|
+
publishDate: r.publish_date || null,
|
|
53
|
+
}));
|
|
54
|
+
return { engine, items };
|
|
55
|
+
}
|
|
56
|
+
async readPage(conn, _settings, input, signal) {
|
|
57
|
+
const data = await this.post('reader', conn, {
|
|
58
|
+
url: input.url,
|
|
59
|
+
return_format: 'markdown',
|
|
60
|
+
// 剥掉图片:远程图片链接模型看不了(ViewImage 只读工作空间),留着
|
|
61
|
+
// 只是往上下文里灌一堆没用的长 URL。
|
|
62
|
+
retain_images: false,
|
|
63
|
+
timeout: READER_FETCH_TIMEOUT_SECONDS,
|
|
64
|
+
}, signal);
|
|
65
|
+
const r = data.reader_result;
|
|
66
|
+
if (!r || typeof r.content !== 'string') {
|
|
67
|
+
throw new WebSearchUpstreamError('zhipu reader 响应缺少 reader_result.content');
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
url: r.url || input.url,
|
|
71
|
+
title: r.title || null,
|
|
72
|
+
description: r.description || null,
|
|
73
|
+
content: r.content,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async post(path, conn, body, signal) {
|
|
77
|
+
const label = `zhipu ${path}`;
|
|
78
|
+
const res = await postJson(label, joinUrl(conn.baseUrl, path), conn, body, signal);
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
const parsed = parseErrorBody(res.text);
|
|
81
|
+
const detail = parsed.message ?? clip(res.text);
|
|
82
|
+
throw new WebSearchUpstreamError(`${label} ${res.status}${detail ? `: ${detail}` : ''}`, {
|
|
83
|
+
code: parsed.code,
|
|
84
|
+
retryable: res.status >= 500 || res.status === 429 || parsed.code === '1701',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const json = parseJson(label, res.text);
|
|
88
|
+
// 智谱部分接口会用 200 + error 体报业务错误,别把它当成功往下传。
|
|
89
|
+
const errBody = json
|
|
90
|
+
.error;
|
|
91
|
+
if (errBody) {
|
|
92
|
+
const code = codeOf(errBody.code);
|
|
93
|
+
throw new WebSearchUpstreamError(`${label} 业务错误${code ? ` ${code}` : ''}:${typeof errBody.message === 'string' ? errBody.message : clip(res.text)}`, { code, retryable: code === '1701' });
|
|
94
|
+
}
|
|
95
|
+
return json;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=zhipu.adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zhipu.adapter.js","sourceRoot":"","sources":["../../src/web-search/zhipu.adapter.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qjp88995/provider-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Client for the provider center: vendor adapters for the AI SDK, model resolution through the center's catalog and passthrough proxy, and a direct mode for local development.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
"@ai-sdk/google": "^4.0.104",
|
|
34
34
|
"@ai-sdk/moonshotai": "^3.0.76",
|
|
35
35
|
"@ai-sdk/openai": "^4.0.101",
|
|
36
|
-
"@ai-sdk/openai-compatible": "^3.0.77"
|
|
36
|
+
"@ai-sdk/openai-compatible": "^3.0.77",
|
|
37
|
+
"fflate": "^0.8.3",
|
|
38
|
+
"zod": "^4.4.3"
|
|
37
39
|
},
|
|
38
40
|
"peerDependencies": {
|
|
39
41
|
"ai": "^7.0.150"
|
|
@@ -43,7 +45,6 @@
|
|
|
43
45
|
"ai": "^7.0.150",
|
|
44
46
|
"typescript": "^5.9.3",
|
|
45
47
|
"vitest": "^4.1.10",
|
|
46
|
-
"zod": "^4.4.3",
|
|
47
48
|
"@pc/shared": "0.1.0"
|
|
48
49
|
},
|
|
49
50
|
"scripts": {
|
package/src/direct-resolver.ts
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { parseCenterError, ProviderClientError } from '../errors.js';
|
|
2
|
+
import { PC_HEADERS } from '../types.js';
|
|
3
|
+
import { type ImageConnection, ImageUpstreamError } from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* POST 一段 JSON,返回状态码与响应正文(同 web-search 的 `postJson`)。只处理「请求没发出去」与「中心自己报的错」:
|
|
7
|
+
* 中止原样抛;网络错误、中心的 `upstream_error` 是可重试的上游错误;中心别的错(没有可用 key、不许用这家……)是配置问题,
|
|
8
|
+
* 抛 `ProviderClientError`。厂商的状态码与响应体怎么解释是适配器的事。
|
|
9
|
+
*/
|
|
10
|
+
export async function postJson(
|
|
11
|
+
label: string,
|
|
12
|
+
url: string,
|
|
13
|
+
{ apiKey, fetch }: ImageConnection,
|
|
14
|
+
body: Record<string, unknown>,
|
|
15
|
+
signal: AbortSignal
|
|
16
|
+
): Promise<{ status: number; ok: boolean; text: string }> {
|
|
17
|
+
let res: Response;
|
|
18
|
+
try {
|
|
19
|
+
res = await fetch(url, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: {
|
|
22
|
+
...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
|
|
23
|
+
'content-type': 'application/json',
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify(body),
|
|
26
|
+
signal,
|
|
27
|
+
});
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (signal.aborted) throw err;
|
|
30
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
31
|
+
throw new ImageUpstreamError(`${label} 请求失败:${reason}`, {
|
|
32
|
+
retryable: true,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const text = await res.text();
|
|
36
|
+
if (!res.ok && !res.headers.has(PC_HEADERS.credential)) {
|
|
37
|
+
const center = centerErrorOf(text);
|
|
38
|
+
if (center?.code === 'upstream_error') {
|
|
39
|
+
throw new ImageUpstreamError(`${label} 请求失败:${center.message}`, {
|
|
40
|
+
code: center.code,
|
|
41
|
+
retryable: true,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (center) {
|
|
45
|
+
throw new ProviderClientError(center.code, center.message, res.status);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { status: res.status, ok: res.ok, text };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function centerErrorOf(text: string) {
|
|
52
|
+
try {
|
|
53
|
+
return parseCenterError(JSON.parse(text));
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 错误详情别整段回灌(HTML 错误页可能几十 KB)。 */
|
|
60
|
+
export function clip(text: string): string {
|
|
61
|
+
const flat = text.replace(/\s+/g, ' ').trim();
|
|
62
|
+
return flat.length > 300 ? `${flat.slice(0, 300)}…` : flat;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 端点根 + 子路径(根上多余的 `/` 去掉)。 */
|
|
66
|
+
export function joinUrl(baseUrl: string, path: string): string {
|
|
67
|
+
return `${baseUrl.replace(/\/+$/, '')}/${path}`;
|
|
68
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fitImageSize,
|
|
3
|
+
type GenerateImageInput,
|
|
4
|
+
type GenerateImageOutcome,
|
|
5
|
+
type ImageAdapter,
|
|
6
|
+
type ImageConnection,
|
|
7
|
+
type ImageTraits,
|
|
8
|
+
} from './types.js';
|
|
9
|
+
import { VolcengineImageAdapter } from './volcengine.adapter.js';
|
|
10
|
+
|
|
11
|
+
/** kind → 生图适配器。中心哪几家能录生图模型(`VendorKindInfo.imageGeneration`)与这里一一对应(契约测试钉着)。 */
|
|
12
|
+
export const IMAGE_ADAPTERS = {
|
|
13
|
+
volcengine: new VolcengineImageAdapter(),
|
|
14
|
+
} as const satisfies Record<string, ImageAdapter>;
|
|
15
|
+
|
|
16
|
+
export type ImageKind = keyof typeof IMAGE_ADAPTERS;
|
|
17
|
+
export const IMAGE_KINDS = Object.keys(IMAGE_ADAPTERS) as ImageKind[];
|
|
18
|
+
|
|
19
|
+
export function isImageKind(kind: string): kind is ImageKind {
|
|
20
|
+
return Object.hasOwn(IMAGE_ADAPTERS, kind);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 一个生图模型,连接已绑好。 */
|
|
24
|
+
export interface ImageGenerator {
|
|
25
|
+
readonly kind: ImageKind;
|
|
26
|
+
readonly modelId: string;
|
|
27
|
+
readonly traits: ImageTraits;
|
|
28
|
+
/** 能不能带参考图(目录里的 `reference-image` 能力)。 */
|
|
29
|
+
readonly referenceImages: boolean;
|
|
30
|
+
generate(
|
|
31
|
+
input: GenerateImageInput,
|
|
32
|
+
signal: AbortSignal
|
|
33
|
+
): Promise<GenerateImageOutcome>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CreateImageGeneratorInput {
|
|
37
|
+
kind: ImageKind;
|
|
38
|
+
modelId: string;
|
|
39
|
+
/** 目录里这个模型有 `reference-image` 能力。 */
|
|
40
|
+
referenceImages: boolean;
|
|
41
|
+
connection: ImageConnection;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 绑好连接的生图模型。中心模式用 `ProviderClient.imageModel`;这里是直连(测试)与中心模式共用的底层。
|
|
46
|
+
* 生成前先校验(空提示词、参考图不支持 / 超张数)并把尺寸放进这家的限制(`fitImageSize`)。
|
|
47
|
+
*/
|
|
48
|
+
export function createImageGenerator(
|
|
49
|
+
input: CreateImageGeneratorInput
|
|
50
|
+
): ImageGenerator {
|
|
51
|
+
const adapter: ImageAdapter = IMAGE_ADAPTERS[input.kind];
|
|
52
|
+
const traits = adapter.traits(input.modelId);
|
|
53
|
+
return {
|
|
54
|
+
kind: input.kind,
|
|
55
|
+
modelId: input.modelId,
|
|
56
|
+
traits,
|
|
57
|
+
referenceImages: input.referenceImages,
|
|
58
|
+
generate: (req, signal) => {
|
|
59
|
+
if (!req.prompt.trim()) {
|
|
60
|
+
return Promise.reject(new RangeError('prompt must not be empty'));
|
|
61
|
+
}
|
|
62
|
+
const refs = req.referenceImages ?? [];
|
|
63
|
+
if (refs.length > 0 && !input.referenceImages) {
|
|
64
|
+
return Promise.reject(
|
|
65
|
+
new RangeError(
|
|
66
|
+
`model "${input.modelId}" does not take reference images`
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (refs.length > traits.maxReferenceImages) {
|
|
71
|
+
return Promise.reject(
|
|
72
|
+
new RangeError(
|
|
73
|
+
`at most ${traits.maxReferenceImages} reference images, got ${refs.length}`
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return adapter.generate(
|
|
78
|
+
input.connection,
|
|
79
|
+
input.modelId,
|
|
80
|
+
{ ...req, size: fitImageSize(req.size, traits) },
|
|
81
|
+
signal
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export * from './types.js';
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { VendorFetch } from '../vendors/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 生图的厂商适配。适配器只管「这家的请求长什么样、响应怎么翻、哪些失败值得重试」与这家的尺寸限制;
|
|
5
|
+
* 什么时候生、生几张、提示词怎么写是 APP 的事。**一次调用出一张图**(火山 5.5 不支持一次多张):要几张就并发几次。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface ImageSize {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 参考图(图生图 / 改图)。字节随请求发出(data URI),不依赖公网地址。 */
|
|
14
|
+
export interface ReferenceImage {
|
|
15
|
+
data: Uint8Array;
|
|
16
|
+
/** `image/jpeg`、`image/png`、`image/webp` …… */
|
|
17
|
+
mimeType: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const IMAGE_FORMATS = ['jpeg', 'png'] as const;
|
|
21
|
+
export type ImageFormat = (typeof IMAGE_FORMATS)[number];
|
|
22
|
+
|
|
23
|
+
export interface GenerateImageInput {
|
|
24
|
+
prompt: string;
|
|
25
|
+
/**
|
|
26
|
+
* 想要的像素尺寸。这家的面积上下限之外会**等比**缩放进去(比例不变),实际尺寸看结果;
|
|
27
|
+
* 想先知道就用 `fitImageSize(size, traits)`。
|
|
28
|
+
*/
|
|
29
|
+
size: ImageSize;
|
|
30
|
+
/** 参考图;模型没有 `reference-image` 能力或超过 `traits.maxReferenceImages` 张就报错,不悄悄丢。 */
|
|
31
|
+
referenceImages?: readonly ReferenceImage[];
|
|
32
|
+
/** 缺省 `jpeg`(幻灯片配图用 jpeg 小一个量级)。 */
|
|
33
|
+
format?: ImageFormat;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface GeneratedImage {
|
|
37
|
+
data: Uint8Array;
|
|
38
|
+
mimeType: string;
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ImageUsage {
|
|
44
|
+
generatedImages: number;
|
|
45
|
+
inputImages: number;
|
|
46
|
+
/** 厂商报的 token 数(火山按面积折算);不报为 null。计价按张,这个只做记录。 */
|
|
47
|
+
tokens: number | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface GenerateImageOutcome {
|
|
51
|
+
image: GeneratedImage;
|
|
52
|
+
usage: ImageUsage;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 这家(这个模型)生图的限制,给调用方定尺寸、写工具说明用。 */
|
|
56
|
+
export interface ImageTraits {
|
|
57
|
+
/** 面积下限(像素)。 */
|
|
58
|
+
minPixels: number;
|
|
59
|
+
/** 面积上限(像素)。 */
|
|
60
|
+
maxPixels: number;
|
|
61
|
+
/** 宽高比(长边 / 短边)上限。 */
|
|
62
|
+
maxAspectRatio: number;
|
|
63
|
+
/** 一次最多带几张参考图。 */
|
|
64
|
+
maxReferenceImages: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 调一次上游要的东西:这家生图所用端点的根地址与出站 fetch。中心模式下 fetch 是中心的透传 fetch、不带 key;
|
|
69
|
+
* 直连模式给 `apiKey`。
|
|
70
|
+
*/
|
|
71
|
+
export interface ImageConnection {
|
|
72
|
+
baseUrl: string;
|
|
73
|
+
fetch: VendorFetch;
|
|
74
|
+
apiKey?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ImageAdapter<K extends string = string> {
|
|
78
|
+
readonly kind: K;
|
|
79
|
+
/** 生图请求发往这家哪个端点(取它的根地址)。 */
|
|
80
|
+
readonly endpointProtocol: 'openai-chat-completions';
|
|
81
|
+
traits(modelId: string): ImageTraits;
|
|
82
|
+
generate(
|
|
83
|
+
conn: ImageConnection,
|
|
84
|
+
modelId: string,
|
|
85
|
+
input: GenerateImageInput,
|
|
86
|
+
signal: AbortSignal
|
|
87
|
+
): Promise<GenerateImageOutcome>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 厂商的失败(HTTP 错误、拒绝生成、响应不成形、没拿到响应)。中心自己报的错是 `ProviderClientError`。
|
|
92
|
+
* 调用方只读 `retryable` 决定要不要再试,不解析消息文本。
|
|
93
|
+
*/
|
|
94
|
+
export class ImageUpstreamError extends Error {
|
|
95
|
+
override readonly name = 'ImageUpstreamError';
|
|
96
|
+
/** 上游错误码(火山的 `InvalidParameter`、`OutputImageSensitiveContentDetected` ……),没有就是 null。 */
|
|
97
|
+
readonly code: string | null;
|
|
98
|
+
readonly retryable: boolean;
|
|
99
|
+
/** 被内容审核拦下(换提示词再试;原样重试没用)。 */
|
|
100
|
+
readonly contentFiltered: boolean;
|
|
101
|
+
|
|
102
|
+
constructor(
|
|
103
|
+
reason: string,
|
|
104
|
+
opts: { code?: string; retryable?: boolean; contentFiltered?: boolean } = {}
|
|
105
|
+
) {
|
|
106
|
+
super(reason);
|
|
107
|
+
this.code = opts.code ?? null;
|
|
108
|
+
this.retryable = opts.retryable ?? false;
|
|
109
|
+
this.contentFiltered = opts.contentFiltered ?? false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 把想要的尺寸放进这家的限制:宽高比超了先按上限收窄短边;面积超了等比缩小、不够等比放大。取整后面积仍要在界内。
|
|
115
|
+
*/
|
|
116
|
+
export function fitImageSize(size: ImageSize, traits: ImageTraits): ImageSize {
|
|
117
|
+
let { width: w, height: h } = size;
|
|
118
|
+
if (!(w > 0 && h > 0)) throw new RangeError('image size must be positive');
|
|
119
|
+
const ratio = Math.max(w, h) / Math.min(w, h);
|
|
120
|
+
if (ratio > traits.maxAspectRatio) {
|
|
121
|
+
if (w > h) h = w / traits.maxAspectRatio;
|
|
122
|
+
else w = h / traits.maxAspectRatio;
|
|
123
|
+
}
|
|
124
|
+
const area = w * h;
|
|
125
|
+
const scale =
|
|
126
|
+
area > traits.maxPixels
|
|
127
|
+
? Math.sqrt(traits.maxPixels / area)
|
|
128
|
+
: area < traits.minPixels
|
|
129
|
+
? Math.sqrt(traits.minPixels / area)
|
|
130
|
+
: 1;
|
|
131
|
+
// 缩小向下取整、放大向上取整,取整后面积不越界。
|
|
132
|
+
const round = scale < 1 ? Math.floor : scale > 1 ? Math.ceil : Math.round;
|
|
133
|
+
return { width: round(w * scale), height: round(h * scale) };
|
|
134
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { clip, joinUrl, postJson } from './http.js';
|
|
2
|
+
import {
|
|
3
|
+
type GenerateImageInput,
|
|
4
|
+
type GenerateImageOutcome,
|
|
5
|
+
type ImageAdapter,
|
|
6
|
+
type ImageConnection,
|
|
7
|
+
type ImageTraits,
|
|
8
|
+
ImageUpstreamError,
|
|
9
|
+
} from './types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 火山方舟(Seedream)生图:`POST {对话端点根}/images/generations`(`https://ark.cn-beijing.volces.com/api/v3`),
|
|
13
|
+
* OpenAI 形状。2026-11-13 经中心透传实测(Seedream 5.5 / 5.0 flash):
|
|
14
|
+
* - `size` 收 `WxH`,面积 921600~4624220 像素,比例宽松(16:1 也行),像素不用对齐;
|
|
15
|
+
* - 一次一张(`n` 被忽略,组图 5.5 不支持);参考图 `image` 收 data URI,最多 10 张;
|
|
16
|
+
* - 返回链接 24 小时失效 → 要 `b64_json`;`watermark: false` 去掉右下角「AI 生成」;
|
|
17
|
+
* - 失败是 `{ error: { code, message } }`(`InvalidParameter` / `MissingParameter` …)。
|
|
18
|
+
*/
|
|
19
|
+
const TRAITS: ImageTraits = {
|
|
20
|
+
minPixels: 921_600,
|
|
21
|
+
maxPixels: 4_624_220,
|
|
22
|
+
maxAspectRatio: 16,
|
|
23
|
+
maxReferenceImages: 10,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
interface ArkImagesBody {
|
|
27
|
+
data?: {
|
|
28
|
+
b64_json?: string;
|
|
29
|
+
size?: string;
|
|
30
|
+
output_format?: string;
|
|
31
|
+
error?: { code?: string; message?: string };
|
|
32
|
+
}[];
|
|
33
|
+
usage?: {
|
|
34
|
+
generated_images?: number;
|
|
35
|
+
input_images?: number;
|
|
36
|
+
total_tokens?: number;
|
|
37
|
+
};
|
|
38
|
+
error?: { code?: string; message?: string };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const LABEL = '火山方舟生图';
|
|
42
|
+
|
|
43
|
+
export class VolcengineImageAdapter implements ImageAdapter<'volcengine'> {
|
|
44
|
+
readonly kind = 'volcengine' as const;
|
|
45
|
+
readonly endpointProtocol = 'openai-chat-completions' as const;
|
|
46
|
+
|
|
47
|
+
traits(): ImageTraits {
|
|
48
|
+
return TRAITS;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async generate(
|
|
52
|
+
conn: ImageConnection,
|
|
53
|
+
modelId: string,
|
|
54
|
+
input: GenerateImageInput,
|
|
55
|
+
signal: AbortSignal
|
|
56
|
+
): Promise<GenerateImageOutcome> {
|
|
57
|
+
const format = input.format ?? 'jpeg';
|
|
58
|
+
const refs = input.referenceImages ?? [];
|
|
59
|
+
const body: Record<string, unknown> = {
|
|
60
|
+
model: modelId,
|
|
61
|
+
prompt: input.prompt,
|
|
62
|
+
size: `${input.size.width}x${input.size.height}`,
|
|
63
|
+
response_format: 'b64_json',
|
|
64
|
+
output_format: format,
|
|
65
|
+
watermark: false,
|
|
66
|
+
};
|
|
67
|
+
if (refs.length > 0) {
|
|
68
|
+
const uris = refs.map(
|
|
69
|
+
r =>
|
|
70
|
+
`data:${r.mimeType};base64,${Buffer.from(r.data).toString('base64')}`
|
|
71
|
+
);
|
|
72
|
+
body.image = uris.length === 1 ? uris[0] : uris;
|
|
73
|
+
}
|
|
74
|
+
const res = await postJson(
|
|
75
|
+
LABEL,
|
|
76
|
+
joinUrl(conn.baseUrl, 'images/generations'),
|
|
77
|
+
conn,
|
|
78
|
+
body,
|
|
79
|
+
signal
|
|
80
|
+
);
|
|
81
|
+
let parsed: ArkImagesBody;
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(res.text) as ArkImagesBody;
|
|
84
|
+
} catch {
|
|
85
|
+
throw new ImageUpstreamError(
|
|
86
|
+
`${LABEL}失败(HTTP ${res.status}):${clip(res.text)}`,
|
|
87
|
+
{ retryable: res.status === 429 || res.status >= 500 }
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (!res.ok || parsed.error) {
|
|
91
|
+
throw upstreamError(res.status, parsed.error);
|
|
92
|
+
}
|
|
93
|
+
const item = parsed.data?.[0];
|
|
94
|
+
if (item?.error) throw upstreamError(res.status, item.error);
|
|
95
|
+
if (!item?.b64_json) {
|
|
96
|
+
throw new ImageUpstreamError(`${LABEL}没有返回图片:${clip(res.text)}`, {
|
|
97
|
+
retryable: true,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const [w, h] = (item.size ?? '').split('x').map(Number);
|
|
101
|
+
const outFormat = item.output_format === 'png' ? 'png' : format;
|
|
102
|
+
return {
|
|
103
|
+
image: {
|
|
104
|
+
data: new Uint8Array(Buffer.from(item.b64_json, 'base64')),
|
|
105
|
+
mimeType: outFormat === 'png' ? 'image/png' : 'image/jpeg',
|
|
106
|
+
width: w && Number.isFinite(w) ? w : input.size.width,
|
|
107
|
+
height: h && Number.isFinite(h) ? h : input.size.height,
|
|
108
|
+
},
|
|
109
|
+
usage: {
|
|
110
|
+
generatedImages: parsed.usage?.generated_images ?? 1,
|
|
111
|
+
inputImages: parsed.usage?.input_images ?? refs.length,
|
|
112
|
+
tokens: parsed.usage?.total_tokens ?? null,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function upstreamError(
|
|
119
|
+
status: number,
|
|
120
|
+
error: { code?: string; message?: string } | undefined
|
|
121
|
+
): ImageUpstreamError {
|
|
122
|
+
const code = error?.code;
|
|
123
|
+
const message = error?.message ?? '未知错误';
|
|
124
|
+
return new ImageUpstreamError(
|
|
125
|
+
`${LABEL}失败(HTTP ${status}${code ? `,${code}` : ''}):${clip(message)}`,
|
|
126
|
+
{
|
|
127
|
+
...(code ? { code } : {}),
|
|
128
|
+
// 限流、服务端故障可重试;参数错、审核拦下重试没用。
|
|
129
|
+
retryable: status === 429 || status >= 500,
|
|
130
|
+
contentFiltered: !!code && /Sensitive|Moderation|RiskControl/i.test(code),
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
}
|