@wuyaos/pi-model-config 0.1.3 → 0.1.4
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/README.md +8 -0
- package/model-config.schema.json +7 -1
- package/package.json +1 -1
- package/src/command.ts +21 -5
- package/src/config.ts +4 -0
- package/src/i18n.ts +10 -0
- package/src/image-apis/common.ts +219 -0
- package/src/image-apis/dashscope.ts +42 -0
- package/src/image-apis/gemini.ts +36 -0
- package/src/image-apis/index.ts +15 -0
- package/src/image-apis/openai.ts +32 -0
- package/src/image-apis/xai.ts +27 -0
- package/src/runtime.ts +20 -3
- package/src/transport.ts +3 -2
- package/src/types.ts +4 -2
package/README.md
CHANGED
|
@@ -122,6 +122,12 @@ Chat 写入 Pi 标准 `models.json`。Classifier 和 image 通过 Pi 的原生 m
|
|
|
122
122
|
支持的 image API:
|
|
123
123
|
|
|
124
124
|
- `openrouter-images`
|
|
125
|
+
- `openai-images`(OpenAI Images;默认 `https://api.openai.com/v1`)
|
|
126
|
+
- `gemini-images`(Gemini GenerateContent;默认 `https://generativelanguage.googleapis.com`)
|
|
127
|
+
- `xai-images`(xAI Images;默认 `https://api.x.ai/v1`;当前将 base64 结果标为 `image/jpeg`,待真实服务商响应验证)
|
|
128
|
+
- `dashscope-images`(DashScope 同步 multimodal-generation;默认 `https://dashscope.aliyuncs.com/api/v1`)
|
|
129
|
+
|
|
130
|
+
这四个新增协议由本插件直接实现请求级 `generateImages()`,**不经过宿主图像管线**。模型级 Base URL 优先,其次服务商 Base URL,最后才采用上列协议默认值;`openrouter-images` 与未知/自定义协议没有默认 URL,必须由用户明确填写。图像响应不计算 token/cost usage(保持为空)。DashScope 固定使用同步 `/services/aigc/multimodal-generation/generation`,解析 `output.choices[0].message.content[].image`(URL 字符串或 `{ url }`);不发送 `X-DashScope-Async`。返回 URL 会经同源重定向、内网地址、30 秒和 10 MiB 限制后下载。
|
|
125
131
|
|
|
126
132
|
支持的 chat API:
|
|
127
133
|
|
|
@@ -188,6 +194,7 @@ Chat 写入 Pi 标准 `models.json`。Classifier 和 image 通过 Pi 的原生 m
|
|
|
188
194
|
|
|
189
195
|
- 行为调研参考了 @indexyz/pi-provider-sub2api(MIT)对 Codex manifest、slug/display_name 和保守失败策略的说明,以及 @router-for-me/pi-cliproxyapi-provider(MIT)公开的 endpoint 约定。
|
|
190
196
|
- 同时查看了 @keywaytech/pi-provider-manager 的发布包行为;其 package 标记 UNLICENSED,因此本项目没有复制或改写其源码,只独立实现了协议 profile、认证头和安全读取逻辑。
|
|
197
|
+
- 图像协议行为还参考本机 `img-research` 中 `@amaster.ai/pi-image-gen` 发布包的 Apache-2.0 `dist/providers/{openai,gemini,dashscope}.js`,以及其依赖 `pi-image-gen-core/src/providers/xai.ts`;仅用于核实公开 endpoint、body 和响应形状,没有复制源码、宿主管线或 UI。
|
|
191
198
|
- 本实现没有照搬上述包的缓存、client_version、名称正则推理、provider 注册或 UI 代码;尤其不会仅凭 CLIProxy 名称添加查询参数。
|
|
192
199
|
|
|
193
200
|
## 已知限制
|
|
@@ -199,3 +206,4 @@ Chat 写入 Pi 标准 `models.json`。Classifier 和 image 通过 Pi 的原生 m
|
|
|
199
206
|
- 插件自有 provider 的“复用 Pi auth”仅处理同 ID 的 API-key credential;OAuth 只适用于未被插件接管、继续使用原生认证实现的 provider,不能移植到另一个自定义 provider ID。
|
|
200
207
|
- 第三方 extension provider 若不由本插件接管,只能管理 chat overlay,不能启用本插件代理或匿名 transport。
|
|
201
208
|
- 保存模型配置后需要 `/reload` 才能更新 extension transport 与 classifier/image 注册;仅切换界面语言不需要 reload。
|
|
209
|
+
- 图像 API 只覆盖文本提示词和各协议的最小一图请求;没有接入宿主图像管线、编辑/多图输入或 usage/cost 计费统计。
|
package/model-config.schema.json
CHANGED
package/package.json
CHANGED
package/src/command.ts
CHANGED
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
} from "./panel.ts";
|
|
25
25
|
import { candidateFromExternal, candidateFromInstalled, candidateFromManaged, mergeCandidates, selectModels, type ModelPanelState } from "./selector.ts";
|
|
26
26
|
import { CHAT_APIS, CLASSIFIER_APIS, IMAGE_APIS, PROXY_CHAT_APIS } from "./transport.ts";
|
|
27
|
+
import { resolveImageBaseUrl } from "./image-apis/common.ts";
|
|
28
|
+
import { SELF_IMAGE_APIS } from "./image-apis/index.ts";
|
|
27
29
|
import type {
|
|
28
30
|
CandidateModel,
|
|
29
31
|
ManagedModel,
|
|
@@ -102,6 +104,14 @@ function modelTypeLabel(language: UiLanguage, type: ManagedModel["type"]): strin
|
|
|
102
104
|
: t(language, "selectorTypeChat");
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
function imageApiLabel(language: UiLanguage, api: string): string {
|
|
108
|
+
const key = api === "openai-images" ? "imageApiOpenai"
|
|
109
|
+
: api === "gemini-images" ? "imageApiGemini"
|
|
110
|
+
: api === "xai-images" ? "imageApiXai"
|
|
111
|
+
: api === "dashscope-images" ? "imageApiDashscope" : "imageApiOpenrouter";
|
|
112
|
+
return `${t(language, key)} (${api})`;
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
export function parseManagedChat(raw: Record<string, unknown>, provider: ModelsJsonProvider, config: ProviderConnectionConfig, applyDefaults = false): ManagedModel | undefined {
|
|
106
116
|
if (typeof raw.id !== "string") return undefined;
|
|
107
117
|
const api = typeof raw.api === "string" ? raw.api : (provider.api ?? config.chatApi ?? "openai-completions");
|
|
@@ -286,8 +296,10 @@ async function editConnectionPanel(ctx: ExtensionCommandContext, language: UiLan
|
|
|
286
296
|
break;
|
|
287
297
|
}
|
|
288
298
|
case "imageApi": {
|
|
289
|
-
const
|
|
290
|
-
|
|
299
|
+
const choices = [...IMAGE_APIS].map((api) => ({ api, label: imageApiLabel(language, api) }));
|
|
300
|
+
const value = await ctx.ui.select("Image API", choices.map((choice) => choice.label));
|
|
301
|
+
const selected = choices.find((choice) => choice.label === value);
|
|
302
|
+
if (selected) draft.imageApi = selected.api;
|
|
291
303
|
break;
|
|
292
304
|
}
|
|
293
305
|
}
|
|
@@ -326,10 +338,13 @@ function validateSelection(
|
|
|
326
338
|
if (draft.proxyUrl) validateHttpUrl(draft.proxyUrl, "Provider proxy URL");
|
|
327
339
|
if (draft.owned) {
|
|
328
340
|
const missingExistingUrl = (existingProvider?.models ?? []).some((model) => typeof model.baseUrl !== "string" && !draft.baseUrl);
|
|
329
|
-
const
|
|
341
|
+
const effectiveModelUrl = (model: ManagedModel) => model.type === "image"
|
|
342
|
+
? resolveImageBaseUrl(model.api, model.baseUrl, draft.baseUrl)
|
|
343
|
+
: model.baseUrl ?? draft.baseUrl;
|
|
344
|
+
const missingSelectedUrl = selected.some((model) => !effectiveModelUrl(model));
|
|
330
345
|
if (missingExistingUrl || missingSelectedUrl) throw mcError("MCFG_E_BASE_URL_REQUIRED");
|
|
331
346
|
for (const model of existingProvider?.models ?? []) validateHttpUrl(String(model.baseUrl ?? draft.baseUrl), `${draft.id}/${String(model.id)} Base URL`);
|
|
332
|
-
for (const model of selected) validateHttpUrl(model
|
|
347
|
+
for (const model of selected) validateHttpUrl(effectiveModelUrl(model)!, `${draft.id}/${model.id} Base URL`);
|
|
333
348
|
}
|
|
334
349
|
if (!draft.owned && selected.some((model) => model.type !== "chat")) throw mcError("MCFG_E_NOT_OWNED", { provider: draft.id });
|
|
335
350
|
if (!draft.owned && (draft.proxyUrl || draft.authMode === "anonymous") && !new Set<string>(getBuiltinProviders()).has(draft.id)) {
|
|
@@ -351,7 +366,8 @@ function validateSelection(
|
|
|
351
366
|
const supported = model.type === "chat" ? CHAT_APIS : model.type === "classifier" ? CLASSIFIER_APIS : IMAGE_APIS;
|
|
352
367
|
if (!supported.has(model.api)) throw mcError("MCFG_E_UNSUPPORTED_API", { identity: `${draft.id}/${model.type}/${model.id}`, api: model.api });
|
|
353
368
|
if (draft.proxyUrl && model.type === "chat" && !PROXY_CHAT_APIS.has(model.api)) throw mcError("MCFG_E_PROXY_UNSUPPORTED", { identity: `${draft.id}/${model.type}/${model.id}` });
|
|
354
|
-
if (draft.authMode === "anonymous" && ["pi-messages", "bedrock-converse-stream", "google-generative-ai", "google-vertex"].includes(model.api)
|
|
369
|
+
if (draft.authMode === "anonymous" && (["pi-messages", "bedrock-converse-stream", "google-generative-ai", "google-vertex"].includes(model.api)
|
|
370
|
+
|| model.type === "image" && SELF_IMAGE_APIS.has(model.api))) {
|
|
355
371
|
throw mcError("MCFG_E_ANONYMOUS_UNSUPPORTED", { identity: `${draft.id}/${model.type}/${model.id}` });
|
|
356
372
|
}
|
|
357
373
|
}
|
package/src/config.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
ModelsJsonProvider,
|
|
22
22
|
ProviderConnectionConfig,
|
|
23
23
|
} from "./types.ts";
|
|
24
|
+
import { IMAGE_API_NAMES } from "./types.ts";
|
|
24
25
|
|
|
25
26
|
export const MODELS_SCHEMA = "https://pi.dev/schemas/models.schema.json";
|
|
26
27
|
export const MODEL_CONFIG_SCHEMA = "https://raw.githubusercontent.com/wuyaos/pi-packages/main/pi-model-config/model-config.schema.json";
|
|
@@ -68,6 +69,9 @@ export function parseModelConfig(source: ConfigSnapshot): ModelConfigFile {
|
|
|
68
69
|
if (provider.discoveryAuthHeader !== undefined && !new Set(["auto", "bearer", "x-api-key", "api-key", "x-goog-api-key", "anthropic", "none"]).has(String(provider.discoveryAuthHeader))) {
|
|
69
70
|
throw mcError("MCFG_E_DISCOVERY_AUTH_HEADER", { file: "model-config.json", provider: id });
|
|
70
71
|
}
|
|
72
|
+
if (provider.imageApi !== undefined && !IMAGE_API_NAMES.includes(String(provider.imageApi) as typeof IMAGE_API_NAMES[number])) {
|
|
73
|
+
throw mcError("MCFG_E_UNSUPPORTED_API", { identity: `${id}/image`, api: String(provider.imageApi) });
|
|
74
|
+
}
|
|
71
75
|
if (provider.models !== undefined && !Array.isArray(provider.models)) throw mcError("MCFG_E_MODELS_ARRAY", { file: "model-config.json", provider: id });
|
|
72
76
|
if (Array.isArray(provider.models) && provider.models.some((model) => !isRecord(model))) throw mcError("MCFG_E_MODELS_ARRAY", { file: "model-config.json", provider: id });
|
|
73
77
|
for (const model of Array.isArray(provider.models) ? provider.models : []) {
|
package/src/i18n.ts
CHANGED
|
@@ -191,6 +191,11 @@ const ZH = {
|
|
|
191
191
|
panelDescChatApi: "聊天模型调用协议;API 名保持原文。",
|
|
192
192
|
panelDescClassifierApi: "判定模型调用协议;API 名保持原文。",
|
|
193
193
|
panelDescImageApi: "图像生成模型调用协议;API 名保持原文。",
|
|
194
|
+
imageApiOpenrouter: "OpenRouter 图像",
|
|
195
|
+
imageApiOpenai: "OpenAI 图像生成",
|
|
196
|
+
imageApiGemini: "Gemini 图像生成",
|
|
197
|
+
imageApiXai: "xAI 图像生成",
|
|
198
|
+
imageApiDashscope: "DashScope 图像生成",
|
|
194
199
|
panelDescContext: "远程模型缺少 context window 时使用的默认值。",
|
|
195
200
|
panelDescMaxTokens: "远程聊天模型缺少 max tokens 时使用的默认值。",
|
|
196
201
|
panelModelName: "模型名称:{value}",
|
|
@@ -523,6 +528,11 @@ const EN = {
|
|
|
523
528
|
panelDescChatApi: "Chat model protocol; API names remain unchanged.",
|
|
524
529
|
panelDescClassifierApi: "Classification model protocol; API names remain unchanged.",
|
|
525
530
|
panelDescImageApi: "Image generation protocol; API names remain unchanged.",
|
|
531
|
+
imageApiOpenrouter: "OpenRouter Images",
|
|
532
|
+
imageApiOpenai: "OpenAI Images",
|
|
533
|
+
imageApiGemini: "Gemini Images",
|
|
534
|
+
imageApiXai: "xAI Images",
|
|
535
|
+
imageApiDashscope: "DashScope Images",
|
|
526
536
|
panelDescContext: "Default used when a remote model omits context window.",
|
|
527
537
|
panelDescMaxTokens: "Default used when a remote chat model omits max tokens.",
|
|
528
538
|
panelModelName: "Model name: {value}",
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { AssistantImages, FetchFunction, ImageModel, ImagesContext, ImagesOptions, ProviderHeaders } from "@earendil-works/pi-ai";
|
|
2
|
+
import { sanitizeError, validateHttpUrl } from "../config.ts";
|
|
3
|
+
|
|
4
|
+
export const IMAGE_API_DEFAULT_BASE_URLS = {
|
|
5
|
+
"openai-images": "https://api.openai.com/v1",
|
|
6
|
+
"gemini-images": "https://generativelanguage.googleapis.com",
|
|
7
|
+
"xai-images": "https://api.x.ai/v1",
|
|
8
|
+
"dashscope-images": "https://dashscope.aliyuncs.com/api/v1",
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export type BuiltinImageApi = keyof typeof IMAGE_API_DEFAULT_BASE_URLS;
|
|
12
|
+
|
|
13
|
+
const ANONYMOUS_SENTINEL = "pi-model-config-anonymous";
|
|
14
|
+
|
|
15
|
+
export function imageDefaultBaseUrl(api: string): string | undefined {
|
|
16
|
+
return IMAGE_API_DEFAULT_BASE_URLS[api as BuiltinImageApi];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Model URLs override provider URLs; protocol defaults only apply to the four self-owned APIs. */
|
|
20
|
+
export function resolveImageBaseUrl(api: string, modelBaseUrl?: string, providerBaseUrl?: string): string | undefined {
|
|
21
|
+
return modelBaseUrl || providerBaseUrl || imageDefaultBaseUrl(api);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function normalizeBaseUrl(value: string, requiredPath?: string): string {
|
|
25
|
+
const url = new URL(validateHttpUrl(value, "Image Base URL"));
|
|
26
|
+
url.search = "";
|
|
27
|
+
url.hash = "";
|
|
28
|
+
url.pathname = url.pathname.replace(/\/+$/u, "") || "/";
|
|
29
|
+
if (requiredPath && !url.pathname.endsWith(requiredPath)) {
|
|
30
|
+
url.pathname = `${url.pathname === "/" ? "" : url.pathname}${requiredPath}`;
|
|
31
|
+
}
|
|
32
|
+
return url.toString().replace(/\/$/u, "");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function promptFromContext(context: ImagesContext): string {
|
|
36
|
+
return context.input.filter((item) => item.type === "text").map((item) => item.text).join("\n").trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function asHeaders(value: ProviderHeaders | undefined): Record<string, string | null | undefined> {
|
|
40
|
+
return { ...(value as Record<string, string | null | undefined> | undefined) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hasHeader(headers: Record<string, string | null | undefined>, name: string): boolean {
|
|
44
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function requestHeaders(
|
|
48
|
+
model: ImageModel<string>,
|
|
49
|
+
options: ImagesOptions | undefined,
|
|
50
|
+
authentication: Record<string, string | undefined>,
|
|
51
|
+
): Record<string, string> {
|
|
52
|
+
const merged = {
|
|
53
|
+
...asHeaders(model.headers),
|
|
54
|
+
...asHeaders(options?.headers),
|
|
55
|
+
};
|
|
56
|
+
for (const [name, value] of Object.entries(authentication)) {
|
|
57
|
+
if (value && !hasHeader(merged, name)) merged[name] = value;
|
|
58
|
+
}
|
|
59
|
+
if (!hasHeader(merged, "content-type")) merged["content-type"] = "application/json";
|
|
60
|
+
return Object.fromEntries(Object.entries(merged).filter((entry): entry is [string, string] => typeof entry[1] === "string"));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function apiKey(options: ImagesOptions | undefined): string | undefined {
|
|
64
|
+
const key = options?.apiKey;
|
|
65
|
+
return key && key !== ANONYMOUS_SENTINEL ? key : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function fetchFor(options: ImagesOptions | undefined): FetchFunction {
|
|
69
|
+
return options?.fetch ?? (globalThis.fetch as FetchFunction);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function imageResult(model: ImageModel<string>): AssistantImages {
|
|
73
|
+
return {
|
|
74
|
+
api: model.api,
|
|
75
|
+
provider: model.provider,
|
|
76
|
+
model: model.id,
|
|
77
|
+
output: [],
|
|
78
|
+
stopReason: "stop",
|
|
79
|
+
timestamp: Date.now(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function activeSecrets(options: ImagesOptions | undefined, extra: readonly string[] = []): string[] {
|
|
84
|
+
const metadata = options?.metadata as { secrets?: unknown } | undefined;
|
|
85
|
+
const supplied = Array.isArray(metadata?.secrets) ? metadata.secrets.filter((value): value is string => typeof value === "string") : [];
|
|
86
|
+
return [...extra, ...supplied, options?.apiKey].filter((value): value is string => !!value && value !== ANONYMOUS_SENTINEL);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function failedImageResult(
|
|
90
|
+
model: ImageModel<string>,
|
|
91
|
+
error: unknown,
|
|
92
|
+
options: ImagesOptions | undefined,
|
|
93
|
+
extraSecrets: readonly string[] = [],
|
|
94
|
+
): AssistantImages {
|
|
95
|
+
const result = imageResult(model);
|
|
96
|
+
result.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
97
|
+
result.errorMessage = sanitizeError(error, activeSecrets(options, extraSecrets));
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function responseJson(response: Response, options?: ImagesOptions, extraSecrets: readonly string[] = []): Promise<unknown> {
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
let detail = "";
|
|
104
|
+
try { detail = (await response.text()).slice(0, 200); } catch { /* status remains useful without a readable body */ }
|
|
105
|
+
const safeDetail = detail ? `: ${sanitizeError(detail, activeSecrets(options, extraSecrets))}` : "";
|
|
106
|
+
throw new Error(`Image API request failed: HTTP ${response.status}${safeDetail}`);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
return await response.json();
|
|
110
|
+
} catch {
|
|
111
|
+
throw new Error("Image API returned invalid JSON");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function records(value: unknown): Record<string, unknown>[] {
|
|
116
|
+
return Array.isArray(value) ? value.filter((item): item is Record<string, unknown> => !!item && typeof item === "object" && !Array.isArray(item)) : [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function dataUrl(mimeType: string, base64: string): string {
|
|
120
|
+
return `data:${mimeType};base64,${base64}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function privateHostname(hostname: string): boolean {
|
|
124
|
+
let normalized = hostname.toLowerCase().replace(/^\[|\]$/gu, "");
|
|
125
|
+
const mappedIpv4 = normalized.match(/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/u);
|
|
126
|
+
const mappedHex = normalized.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/u);
|
|
127
|
+
if (mappedIpv4) normalized = mappedIpv4[1]!;
|
|
128
|
+
else if (mappedHex) {
|
|
129
|
+
const high = Number.parseInt(mappedHex[1]!, 16);
|
|
130
|
+
const low = Number.parseInt(mappedHex[2]!, 16);
|
|
131
|
+
normalized = `${high >>> 8}.${high & 0xff}.${low >>> 8}.${low & 0xff}`;
|
|
132
|
+
}
|
|
133
|
+
if (normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") return true;
|
|
134
|
+
const ipv4 = normalized.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/u)?.slice(1).map(Number);
|
|
135
|
+
if (ipv4 && ipv4.every((part) => part >= 0 && part <= 255)) {
|
|
136
|
+
const [a, b] = ipv4;
|
|
137
|
+
return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
|
|
138
|
+
}
|
|
139
|
+
return normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe80:");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function validatePublicImageUrl(value: string): URL {
|
|
143
|
+
const normalized = validateHttpUrl(value, "DashScope image URL");
|
|
144
|
+
const url = new URL(normalized);
|
|
145
|
+
if (privateHostname(url.hostname)) throw new Error("DashScope image URL targets a private network address");
|
|
146
|
+
return url;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function redirectStatus(status: number): boolean {
|
|
150
|
+
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function timedSignal(signal: AbortSignal | undefined, timeoutMs: number): { signal: AbortSignal; dispose: () => void } {
|
|
154
|
+
const controller = new AbortController();
|
|
155
|
+
const abort = () => controller.abort(signal?.reason);
|
|
156
|
+
if (signal?.aborted) abort();
|
|
157
|
+
else signal?.addEventListener("abort", abort, { once: true });
|
|
158
|
+
const timer = setTimeout(() => controller.abort(new Error("DashScope image download timed out")), timeoutMs);
|
|
159
|
+
return { signal: controller.signal, dispose: () => { clearTimeout(timer); signal?.removeEventListener("abort", abort); } };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function readLimitedBody(response: Response, limit: number): Promise<Uint8Array> {
|
|
163
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
164
|
+
if (Number.isFinite(contentLength) && contentLength > limit) throw new Error(`DashScope image exceeds ${limit} byte limit`);
|
|
165
|
+
if (!response.body) {
|
|
166
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
167
|
+
if (bytes.byteLength > limit) throw new Error(`DashScope image exceeds ${limit} byte limit`);
|
|
168
|
+
return bytes;
|
|
169
|
+
}
|
|
170
|
+
const reader = response.body.getReader();
|
|
171
|
+
const chunks: Uint8Array[] = [];
|
|
172
|
+
let size = 0;
|
|
173
|
+
try {
|
|
174
|
+
while (true) {
|
|
175
|
+
const next = await reader.read();
|
|
176
|
+
if (next.done) break;
|
|
177
|
+
size += next.value.byteLength;
|
|
178
|
+
if (size > limit) { await reader.cancel(); throw new Error(`DashScope image exceeds ${limit} byte limit`); }
|
|
179
|
+
chunks.push(next.value);
|
|
180
|
+
}
|
|
181
|
+
} finally {
|
|
182
|
+
reader.releaseLock();
|
|
183
|
+
}
|
|
184
|
+
const bytes = new Uint8Array(size);
|
|
185
|
+
let offset = 0;
|
|
186
|
+
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
|
|
187
|
+
return bytes;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Downloads only public HTTP(S) URLs, preserves origin across at most five manual redirects, and sends no API headers. */
|
|
191
|
+
export async function downloadDashScopeImage(urlValue: string, fetcher: FetchFunction, signal?: AbortSignal): Promise<{ mimeType: string; data: string }> {
|
|
192
|
+
const initial = validatePublicImageUrl(urlValue);
|
|
193
|
+
const origin = initial.origin;
|
|
194
|
+
const timed = timedSignal(signal, 30_000);
|
|
195
|
+
try {
|
|
196
|
+
let current = initial;
|
|
197
|
+
for (let redirects = 0; redirects <= 5; redirects++) {
|
|
198
|
+
const response = await fetcher(current, { method: "GET", redirect: "manual", signal: timed.signal });
|
|
199
|
+
if (redirectStatus(response.status)) {
|
|
200
|
+
if (redirects === 5) throw new Error("DashScope image redirect limit exceeded");
|
|
201
|
+
const location = response.headers.get("location");
|
|
202
|
+
if (!location) throw new Error(`DashScope image redirect ${response.status} lacks Location`);
|
|
203
|
+
const next = validatePublicImageUrl(new URL(location, current).toString());
|
|
204
|
+
if (next.origin !== origin) throw new Error("DashScope image redirect changed origin");
|
|
205
|
+
await response.body?.cancel();
|
|
206
|
+
current = next;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (!response.ok) throw new Error(`DashScope image download failed: HTTP ${response.status}`);
|
|
210
|
+
const bytes = await readLimitedBody(response, 10 * 1024 * 1024);
|
|
211
|
+
const contentType = response.headers.get("content-type")?.split(";", 1)[0]?.trim();
|
|
212
|
+
const mimeType = contentType?.startsWith("image/") ? contentType : "image/png";
|
|
213
|
+
return { mimeType, data: dataUrl(mimeType, Buffer.from(bytes).toString("base64")) };
|
|
214
|
+
}
|
|
215
|
+
throw new Error("DashScope image redirect limit exceeded");
|
|
216
|
+
} finally {
|
|
217
|
+
timed.dispose();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AssistantImages, ImageModel, ImagesContext, ImagesOptions } from "@earendil-works/pi-ai";
|
|
2
|
+
import { apiKey, downloadDashScopeImage, failedImageResult, fetchFor, imageResult, normalizeBaseUrl, promptFromContext, records, requestHeaders, resolveImageBaseUrl, responseJson } from "./common.ts";
|
|
3
|
+
|
|
4
|
+
/** DashScope multimodal-generation synchronous endpoint. Deliberately does not set X-DashScope-Async. */
|
|
5
|
+
export async function generateImages(model: ImageModel<string>, context: ImagesContext, options?: ImagesOptions, activeSecrets: readonly string[] = []): Promise<AssistantImages> {
|
|
6
|
+
try {
|
|
7
|
+
const baseUrl = resolveImageBaseUrl(model.api, model.baseUrl);
|
|
8
|
+
if (!baseUrl) throw new Error("DashScope Images requires a Base URL");
|
|
9
|
+
const key = apiKey(options);
|
|
10
|
+
if (!key) throw new Error("DashScope Images requires an API key");
|
|
11
|
+
const fetcher = fetchFor(options);
|
|
12
|
+
const response = await fetcher(`${normalizeBaseUrl(baseUrl, "/api/v1")}/services/aigc/multimodal-generation/generation`, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: requestHeaders(model, options, { authorization: `Bearer ${key}` }),
|
|
15
|
+
body: JSON.stringify({
|
|
16
|
+
model: model.id,
|
|
17
|
+
input: { messages: [{ role: "user", content: [{ text: promptFromContext(context) }] }] },
|
|
18
|
+
}),
|
|
19
|
+
signal: options?.signal,
|
|
20
|
+
});
|
|
21
|
+
const json = await responseJson(response, options, activeSecrets) as Record<string, unknown>;
|
|
22
|
+
const output = json.output as Record<string, unknown> | undefined;
|
|
23
|
+
const firstChoice = records(output?.choices)[0];
|
|
24
|
+
const message = firstChoice?.message as Record<string, unknown> | undefined;
|
|
25
|
+
const result = imageResult(model);
|
|
26
|
+
for (const part of records(message?.content)) {
|
|
27
|
+
const imageField = part.image;
|
|
28
|
+
const url = typeof imageField === "string"
|
|
29
|
+
? imageField
|
|
30
|
+
: imageField && typeof imageField === "object" && !Array.isArray(imageField) && typeof (imageField as Record<string, unknown>).url === "string"
|
|
31
|
+
? String((imageField as Record<string, unknown>).url)
|
|
32
|
+
: undefined;
|
|
33
|
+
if (!url) continue;
|
|
34
|
+
const image = await downloadDashScopeImage(url, fetcher, options?.signal);
|
|
35
|
+
result.output.push({ type: "image", mimeType: image.mimeType, data: image.data });
|
|
36
|
+
}
|
|
37
|
+
if (!result.output.length) throw new Error("DashScope Images returned no downloadable image URLs");
|
|
38
|
+
return result;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return failedImageResult(model, error, options, activeSecrets);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AssistantImages, ImageModel, ImagesContext, ImagesOptions } from "@earendil-works/pi-ai";
|
|
2
|
+
import { apiKey, dataUrl, failedImageResult, fetchFor, imageResult, normalizeBaseUrl, promptFromContext, records, requestHeaders, resolveImageBaseUrl, responseJson } from "./common.ts";
|
|
3
|
+
|
|
4
|
+
export async function generateImages(model: ImageModel<string>, context: ImagesContext, options?: ImagesOptions, activeSecrets: readonly string[] = []): Promise<AssistantImages> {
|
|
5
|
+
try {
|
|
6
|
+
const baseUrl = resolveImageBaseUrl(model.api, model.baseUrl);
|
|
7
|
+
if (!baseUrl) throw new Error("Gemini Images requires a Base URL");
|
|
8
|
+
const key = apiKey(options);
|
|
9
|
+
if (!key) throw new Error("Gemini Images requires an API key");
|
|
10
|
+
const response = await fetchFor(options)(`${normalizeBaseUrl(baseUrl, "/v1beta")}/models/${encodeURIComponent(model.id)}:generateContent`, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: requestHeaders(model, options, { "x-goog-api-key": key }),
|
|
13
|
+
body: JSON.stringify({
|
|
14
|
+
contents: [{ role: "user", parts: [{ text: promptFromContext(context) }] }],
|
|
15
|
+
generationConfig: { responseModalities: ["IMAGE"], imageConfig: { imageSize: "1K" } },
|
|
16
|
+
}),
|
|
17
|
+
signal: options?.signal,
|
|
18
|
+
});
|
|
19
|
+
const json = await responseJson(response, options, activeSecrets) as Record<string, unknown>;
|
|
20
|
+
const result = imageResult(model);
|
|
21
|
+
for (const candidate of records(json.candidates)) {
|
|
22
|
+
const content = candidate.content as Record<string, unknown> | undefined;
|
|
23
|
+
for (const part of records(content?.parts)) {
|
|
24
|
+
const inline = (part.inline_data ?? part.inlineData) as Record<string, unknown> | undefined;
|
|
25
|
+
const encoded = typeof inline?.data === "string" ? inline.data : undefined;
|
|
26
|
+
const mime = inline?.mime_type ?? inline?.mimeType;
|
|
27
|
+
const mimeType = typeof mime === "string" ? mime : "image/png";
|
|
28
|
+
if (encoded) result.output.push({ type: "image", data: dataUrl(mimeType, encoded), mimeType });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!result.output.length) throw new Error("Gemini Images returned no inline image data");
|
|
32
|
+
return result;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return failedImageResult(model, error, options, activeSecrets);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ProviderImages } from "@earendil-works/pi-ai";
|
|
2
|
+
import { generateImages as generateDashscopeImages } from "./dashscope.ts";
|
|
3
|
+
import { generateImages as generateGeminiImages } from "./gemini.ts";
|
|
4
|
+
import { generateImages as generateOpenaiImages } from "./openai.ts";
|
|
5
|
+
import { generateImages as generateXaiImages } from "./xai.ts";
|
|
6
|
+
|
|
7
|
+
/** Extension-owned APIs absent from Pi 1.2.0's host image catalog. */
|
|
8
|
+
export const SELF_IMAGE_IMPLEMENTATIONS: Readonly<Record<string, ProviderImages>> = {
|
|
9
|
+
"openai-images": { generateImages: generateOpenaiImages },
|
|
10
|
+
"gemini-images": { generateImages: generateGeminiImages },
|
|
11
|
+
"xai-images": { generateImages: generateXaiImages },
|
|
12
|
+
"dashscope-images": { generateImages: generateDashscopeImages },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const SELF_IMAGE_APIS: ReadonlySet<string> = new Set(Object.keys(SELF_IMAGE_IMPLEMENTATIONS));
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AssistantImages, ImageModel, ImagesContext, ImagesOptions } from "@earendil-works/pi-ai";
|
|
2
|
+
import { apiKey, dataUrl, failedImageResult, fetchFor, imageResult, normalizeBaseUrl, promptFromContext, records, requestHeaders, resolveImageBaseUrl, responseJson } from "./common.ts";
|
|
3
|
+
|
|
4
|
+
/** OpenAI Images API; this is a small request-local adapter, not Pi's host image pipeline. */
|
|
5
|
+
export async function generateImages(model: ImageModel<string>, context: ImagesContext, options?: ImagesOptions, activeSecrets: readonly string[] = []): Promise<AssistantImages> {
|
|
6
|
+
try {
|
|
7
|
+
const baseUrl = resolveImageBaseUrl(model.api, model.baseUrl);
|
|
8
|
+
if (!baseUrl) throw new Error("OpenAI Images requires a Base URL");
|
|
9
|
+
const key = apiKey(options);
|
|
10
|
+
if (!key) throw new Error("OpenAI Images requires an API key");
|
|
11
|
+
const isGptImage = model.id.toLowerCase().startsWith("gpt-image");
|
|
12
|
+
const body: Record<string, unknown> = { model: model.id, prompt: promptFromContext(context), n: 1 };
|
|
13
|
+
if (isGptImage) body.output_format = "png";
|
|
14
|
+
else body.response_format = "b64_json";
|
|
15
|
+
const response = await fetchFor(options)(`${normalizeBaseUrl(baseUrl, "/v1")}/images/generations`, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: requestHeaders(model, options, { authorization: `Bearer ${key}` }),
|
|
18
|
+
body: JSON.stringify(body),
|
|
19
|
+
signal: options?.signal,
|
|
20
|
+
});
|
|
21
|
+
const json = await responseJson(response, options, activeSecrets) as Record<string, unknown>;
|
|
22
|
+
const result = imageResult(model);
|
|
23
|
+
for (const item of records(json.data)) {
|
|
24
|
+
const encoded = typeof item.b64_json === "string" ? item.b64_json : undefined;
|
|
25
|
+
if (encoded) result.output.push({ type: "image", data: dataUrl("image/png", encoded), mimeType: "image/png" });
|
|
26
|
+
}
|
|
27
|
+
if (!result.output.length) throw new Error("OpenAI Images returned no base64 image data");
|
|
28
|
+
return result;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return failedImageResult(model, error, options, activeSecrets);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AssistantImages, ImageModel, ImagesContext, ImagesOptions } from "@earendil-works/pi-ai";
|
|
2
|
+
import { apiKey, dataUrl, failedImageResult, fetchFor, imageResult, normalizeBaseUrl, promptFromContext, records, requestHeaders, resolveImageBaseUrl, responseJson } from "./common.ts";
|
|
3
|
+
|
|
4
|
+
export async function generateImages(model: ImageModel<string>, context: ImagesContext, options?: ImagesOptions, activeSecrets: readonly string[] = []): Promise<AssistantImages> {
|
|
5
|
+
try {
|
|
6
|
+
const baseUrl = resolveImageBaseUrl(model.api, model.baseUrl);
|
|
7
|
+
if (!baseUrl) throw new Error("xAI Images requires a Base URL");
|
|
8
|
+
const key = apiKey(options);
|
|
9
|
+
if (!key) throw new Error("xAI Images requires an API key");
|
|
10
|
+
const response = await fetchFor(options)(`${normalizeBaseUrl(baseUrl, "/v1")}/images/generations`, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: requestHeaders(model, options, { authorization: `Bearer ${key}` }),
|
|
13
|
+
body: JSON.stringify({ model: model.id, prompt: promptFromContext(context), n: 1, response_format: "b64_json" }),
|
|
14
|
+
signal: options?.signal,
|
|
15
|
+
});
|
|
16
|
+
const json = await responseJson(response, options, activeSecrets) as Record<string, unknown>;
|
|
17
|
+
const result = imageResult(model);
|
|
18
|
+
for (const item of records(json.data)) {
|
|
19
|
+
const encoded = typeof item.b64_json === "string" ? item.b64_json : undefined;
|
|
20
|
+
if (encoded) result.output.push({ type: "image", data: dataUrl("image/jpeg", encoded), mimeType: "image/jpeg" });
|
|
21
|
+
}
|
|
22
|
+
if (!result.output.length) throw new Error("xAI Images returned no base64 image data");
|
|
23
|
+
return result;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return failedImageResult(model, error, options, activeSecrets);
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
|
13
13
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import type { ModelConfigFile, ModelsJsonFile, ModelsJsonProvider, ProviderConnectionConfig } from "./types.ts";
|
|
15
15
|
import { validateHttpUrl } from "./config.ts";
|
|
16
|
+
import { resolveImageBaseUrl } from "./image-apis/common.ts";
|
|
17
|
+
import { SELF_IMAGE_APIS, SELF_IMAGE_IMPLEMENTATIONS } from "./image-apis/index.ts";
|
|
16
18
|
import { mcError, t, type UiLanguage } from "./i18n.ts";
|
|
17
19
|
import { ANONYMOUS_SENTINEL, applyProviderTransport, CHAT_APIS, CLASSIFIER_APIS, IMAGE_APIS, PROXY_CHAT_APIS, type TransportOptions } from "./transport.ts";
|
|
18
20
|
|
|
@@ -105,7 +107,9 @@ function wrapClassifier(api: string, config: ProviderConnectionConfig): Provider
|
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
function wrapImages(api: string, config: ProviderConnectionConfig): ProviderImages {
|
|
108
|
-
|
|
110
|
+
// Pi 1.2.0's ImageApi remains open to custom strings, while its host catalog only
|
|
111
|
+
// knows openrouter-images. The four entries below are extension-owned adapters.
|
|
112
|
+
const base = SELF_IMAGE_APIS.has(api) ? SELF_IMAGE_IMPLEMENTATIONS[api]! : requiredImplementation(HOST_APIS.image, api, "image");
|
|
109
113
|
return {
|
|
110
114
|
generateImages: (model, context, options) => base.generateImages(model, context, applyProviderTransport(options, {
|
|
111
115
|
proxyUrl: config.proxyUrl,
|
|
@@ -174,7 +178,14 @@ function validateRuntimeConfig(providerId: string, config: ProviderConnectionCon
|
|
|
174
178
|
const supported = model.type === "classifier" ? CLASSIFIER_APIS : model.type === "image" ? IMAGE_APIS : undefined;
|
|
175
179
|
if (!supported) throw mcError("MCFG_E_MODEL_SHAPE", { file: "model-config.json", provider: providerId });
|
|
176
180
|
if (!supported.has(model.api)) throw mcError("MCFG_E_TYPE_API", { identity: `${providerId}/${model.type}/${model.id}`, api: model.api });
|
|
177
|
-
|
|
181
|
+
const effectiveBaseUrl = model.type === "image"
|
|
182
|
+
? resolveImageBaseUrl(model.api, model.baseUrl, modelsProvider?.baseUrl)
|
|
183
|
+
: model.baseUrl ?? modelsProvider?.baseUrl;
|
|
184
|
+
if (config.owned && !effectiveBaseUrl) throw mcError("MCFG_E_BASE_URL_REQUIRED");
|
|
185
|
+
if (config.owned && effectiveBaseUrl) validateHttpUrl(effectiveBaseUrl, `${providerId}/${model.id} Base URL`);
|
|
186
|
+
if (config.authMode === "anonymous" && model.type === "image" && SELF_IMAGE_APIS.has(model.api)) {
|
|
187
|
+
throw mcError("MCFG_E_ANONYMOUS_UNSUPPORTED", { identity: `${providerId}/image/${model.id}` });
|
|
188
|
+
}
|
|
178
189
|
if (config.owned && model.type === "classifier" && typeof model.contextWindow !== "number") throw mcError("MCFG_E_MODEL_SHAPE", { file: "model-config.json", provider: providerId });
|
|
179
190
|
}
|
|
180
191
|
if (!config.owned && (config.models?.length ?? 0) > 0) {
|
|
@@ -192,7 +203,13 @@ function validateRuntimeConfig(providerId: string, config: ProviderConnectionCon
|
|
|
192
203
|
|
|
193
204
|
function registerOwnedProvider(pi: ExtensionAPI, providerId: string, config: ProviderConnectionConfig, modelsProvider: ModelsJsonProvider, language: UiLanguage): void {
|
|
194
205
|
const chatModels = normalizeChatModels(providerId, modelsProvider, config);
|
|
195
|
-
const extraModels = (config.models ?? []).map((model) => ({
|
|
206
|
+
const extraModels = (config.models ?? []).map((model) => ({
|
|
207
|
+
...model,
|
|
208
|
+
provider: providerId,
|
|
209
|
+
baseUrl: model.type === "image"
|
|
210
|
+
? resolveImageBaseUrl(model.api, model.baseUrl, modelsProvider.baseUrl) ?? ""
|
|
211
|
+
: model.baseUrl ?? modelsProvider.baseUrl ?? "",
|
|
212
|
+
}));
|
|
196
213
|
const allModels = [...chatModels, ...extraModels];
|
|
197
214
|
if (allModels.length === 0) return;
|
|
198
215
|
const chatApis = new Set(chatModels.map((model) => model.api));
|
package/src/transport.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ProxyAgent, fetch as undiciFetch } from "undici";
|
|
2
2
|
import type { FetchFunction, ProviderEnv, ProviderHeaders, Transport } from "@earendil-works/pi-ai";
|
|
3
|
-
import type
|
|
3
|
+
import { IMAGE_API_NAMES, type ProviderDraft } from "./types.ts";
|
|
4
4
|
import { validateHttpUrl } from "./config.ts";
|
|
5
5
|
import { mcError } from "./i18n.ts";
|
|
6
6
|
|
|
@@ -26,7 +26,7 @@ export const CLASSIFIER_APIS = new Set([
|
|
|
26
26
|
"cloudflare-workers-ai-system-one",
|
|
27
27
|
"openai-decisions",
|
|
28
28
|
]);
|
|
29
|
-
export const IMAGE_APIS = new Set(
|
|
29
|
+
export const IMAGE_APIS = new Set(IMAGE_API_NAMES);
|
|
30
30
|
|
|
31
31
|
export function assertSupportedProtocol(draft: ProviderDraft, type: "chat" | "classifier" | "image"): void {
|
|
32
32
|
const api = type === "chat" ? draft.chatApi : type === "classifier" ? draft.classifierApi : draft.imageApi;
|
|
@@ -87,6 +87,7 @@ export function applyProviderTransport<T extends TransportOptions>(
|
|
|
87
87
|
...next.headers,
|
|
88
88
|
authorization: null,
|
|
89
89
|
"x-api-key": null,
|
|
90
|
+
"x-goog-api-key": null,
|
|
90
91
|
"cf-aig-authorization": null,
|
|
91
92
|
};
|
|
92
93
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { AnyModel } from "@earendil-works/pi-ai";
|
|
2
2
|
|
|
3
3
|
export const MODEL_TYPES = ["chat", "classifier", "image"] as const;
|
|
4
|
+
export const IMAGE_API_NAMES = ["openrouter-images", "openai-images", "gemini-images", "xai-images", "dashscope-images"] as const;
|
|
4
5
|
export type ManagedModelType = (typeof MODEL_TYPES)[number];
|
|
6
|
+
export type ImageApiName = (typeof IMAGE_API_NAMES)[number];
|
|
5
7
|
export type ModelIdentity = `${string}\u0000${ManagedModelType}\u0000${string}`;
|
|
6
8
|
|
|
7
9
|
export type AuthMode = "models-json" | "reuse-pi" | "anonymous";
|
|
@@ -64,7 +66,7 @@ export interface ProviderConnectionConfig {
|
|
|
64
66
|
proxyUrl?: string;
|
|
65
67
|
chatApi?: string;
|
|
66
68
|
classifierApi?: string;
|
|
67
|
-
imageApi?:
|
|
69
|
+
imageApi?: ImageApiName;
|
|
68
70
|
defaultType?: ManagedModelType;
|
|
69
71
|
defaultContextWindow?: number;
|
|
70
72
|
defaultMaxTokens?: number;
|
|
@@ -111,7 +113,7 @@ export interface ProviderDraft {
|
|
|
111
113
|
proxyUrl?: string;
|
|
112
114
|
chatApi: string;
|
|
113
115
|
classifierApi: string;
|
|
114
|
-
imageApi:
|
|
116
|
+
imageApi: ImageApiName;
|
|
115
117
|
defaultType: ManagedModelType;
|
|
116
118
|
}
|
|
117
119
|
|