|
|
@@ -14,10 +14,19 @@ export const PROVIDER_ID = 'zsjz';
|
|
|
|
|
|
export type AppliedProvider = AppliedProviderInfo;
|
|
|
|
|
|
-export interface ProviderCompatibility {
|
|
|
- compatible: boolean;
|
|
|
- /** null 表示网络不可达,无法判定 */
|
|
|
- status: number | null;
|
|
|
+/** 端点协议判定:responses 直通;chat-only 走内置桥接;unsupported 双协议都没有;unreachable 网络不可达 */
|
|
|
+export type EndpointProtocol = 'responses' | 'chat-only' | 'unsupported' | 'unreachable';
|
|
|
+
|
|
|
+export interface EndpointProbeResult {
|
|
|
+ protocol: EndpointProtocol;
|
|
|
+ /** /responses 的 HTTP 状态;未探或网络失败为 null */
|
|
|
+ responsesStatus: number | null;
|
|
|
+ /** /chat/completions 的 HTTP 状态;未探为 null */
|
|
|
+ chatStatus: number | null;
|
|
|
+ /** /api/version 探到的 Ollama 版本;非 Ollama 或未探为 null */
|
|
|
+ ollamaVersion: string | null;
|
|
|
+ /** Ollama 低于 0.13.3(没有非状态化 /v1/responses):提示升级,但允许走桥接 */
|
|
|
+ ollamaNeedsUpgrade: boolean;
|
|
|
detail: string;
|
|
|
}
|
|
|
|
|
|
@@ -131,17 +140,15 @@ export function describeDroppedKeys(input: ApplyProviderInput): string[] {
|
|
|
return Object.keys(config ?? {}).filter((key) => !EXTRA_ALLOWLIST.includes(key));
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 探测端点是否实现了 Responses API。
|
|
|
- * 404/405 = 未实现(Codex 0.155 起已下线 wire_api="chat",chat-only 端点用不了);
|
|
|
- * 401/403/400/422/2xx = 路由存在,判为兼容。
|
|
|
- */
|
|
|
-export async function probeResponsesEndpoint(
|
|
|
- baseUrl: string,
|
|
|
- apiKey?: string | null,
|
|
|
-): Promise<ProviderCompatibility> {
|
|
|
- const url = `${baseUrl.replace(/\/+$/u, '')}/responses`;
|
|
|
- let status: number;
|
|
|
+/** Ollama 自 0.13.3 起提供非状态化 /v1/responses(流式 / 工具调用 / reasoning summaries) */
|
|
|
+const OLLAMA_RESPONSES_MIN_VERSION: readonly number[] = [0, 13, 3];
|
|
|
+
|
|
|
+interface ProbeAttempt {
|
|
|
+ status: number | null;
|
|
|
+ error: string | null;
|
|
|
+}
|
|
|
+
|
|
|
+async function postProbe(url: string, body: Record<string, unknown>, apiKey?: string | null): Promise<ProbeAttempt> {
|
|
|
try {
|
|
|
const response = await fetch(url, {
|
|
|
method: 'POST',
|
|
|
@@ -149,25 +156,131 @@ export async function probeResponsesEndpoint(
|
|
|
'content-type': 'application/json',
|
|
|
...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
|
|
|
},
|
|
|
- body: JSON.stringify({ model: 'probe', input: 'probe' }),
|
|
|
+ body: JSON.stringify(body),
|
|
|
signal: AbortSignal.timeout(8_000),
|
|
|
});
|
|
|
- status = response.status;
|
|
|
+ return { status: response.status, error: null };
|
|
|
} catch (error) {
|
|
|
- return {
|
|
|
- compatible: false,
|
|
|
- status: null,
|
|
|
- detail: `端点不可达:${error instanceof Error ? error.message : String(error)}`,
|
|
|
- };
|
|
|
+ return { status: null, error: error instanceof Error ? error.message : String(error) };
|
|
|
}
|
|
|
- if (status === 404 || status === 405) {
|
|
|
- return {
|
|
|
- compatible: false,
|
|
|
- status,
|
|
|
- detail: `该端点未实现 Responses API(HTTP ${status})。Codex 0.155 起已下线 wire_api="chat",只提供 /chat/completions 的服务需要额外网关转换。`,
|
|
|
- };
|
|
|
+}
|
|
|
+
|
|
|
+/** Ollama 的 /api/version 挂在 API 根之外(去掉 /v1 尾段);非 Ollama 端点返回 null */
|
|
|
+async function probeOllamaVersion(baseUrl: string): Promise<string | null> {
|
|
|
+ const origin = baseUrl.replace(/\/v\d+$/u, '');
|
|
|
+ try {
|
|
|
+ const response = await fetch(`${origin}/api/version`, { signal: AbortSignal.timeout(5_000) });
|
|
|
+ if (response.status !== 200) return null;
|
|
|
+ const parsed = (await response.json()) as { version?: unknown };
|
|
|
+ return typeof parsed.version === 'string' && parsed.version ? parsed.version : null;
|
|
|
+ } catch {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function parseVersion(raw: string): number[] | null {
|
|
|
+ const match = raw.trim().match(/^(\d+)\.(\d+)(?:\.(\d+))?/u);
|
|
|
+ if (!match) return null;
|
|
|
+ return [Number(match[1]), Number(match[2]), Number(match[3] ?? 0)];
|
|
|
+}
|
|
|
+
|
|
|
+function versionLt(a: number[], b: readonly number[]): boolean {
|
|
|
+ for (let index = 0; index < 3; index += 1) {
|
|
|
+ if (a[index] !== b[index]) return a[index] < b[index];
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+function probeResult(partial: Partial<EndpointProbeResult> & { protocol: EndpointProtocol; detail: string }): EndpointProbeResult {
|
|
|
+ return {
|
|
|
+ responsesStatus: null,
|
|
|
+ chatStatus: null,
|
|
|
+ ollamaVersion: null,
|
|
|
+ ollamaNeedsUpgrade: false,
|
|
|
+ ...partial,
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 探测端点协议能力(四态判定)。
|
|
|
+ *
|
|
|
+ * Codex 0.155+ 只会发 Responses API:responses → 直通;chat-only → 内置桥接转换;
|
|
|
+ * 其余两种拒绝。判定顺序:
|
|
|
+ * 1. POST /responses(用**真实模型名**——Ollama 对未拉取的模型返回 404,不能直接判端点不支持);
|
|
|
+ * 2. 404/405 且是 Ollama → 查 /api/version 消歧:≥0.13.3 说明 404 只是模型没拉(判 responses),
|
|
|
+ * 低于 0.13.3 直接判 chat-only(/chat/completions 必有);
|
|
|
+ * 3. 否则再 POST /chat/completions:路由存在判 chat-only,也不存在判 unsupported。
|
|
|
+ */
|
|
|
+export async function probeEndpoint(params: {
|
|
|
+ baseUrl: string;
|
|
|
+ modelId?: string | null;
|
|
|
+ apiKey?: string | null;
|
|
|
+ providerType?: string | null;
|
|
|
+ builtinProvider?: string | null;
|
|
|
+}): Promise<EndpointProbeResult> {
|
|
|
+ const baseUrl = params.baseUrl.replace(/\/+$/u, '');
|
|
|
+ const model = params.modelId?.trim() || 'probe';
|
|
|
+ const isOllama =
|
|
|
+ params.builtinProvider === 'ollama' ||
|
|
|
+ `${params.providerType ?? ''}`.trim().toUpperCase() === 'OLLAMA';
|
|
|
+
|
|
|
+ const responses = await postProbe(
|
|
|
+ `${baseUrl}/responses`,
|
|
|
+ { model, input: 'ping', max_output_tokens: 16, stream: false },
|
|
|
+ params.apiKey,
|
|
|
+ );
|
|
|
+ if (responses.status === null) {
|
|
|
+ return probeResult({ protocol: 'unreachable', detail: `端点不可达:${responses.error ?? '网络错误'}` });
|
|
|
+ }
|
|
|
+ if (responses.status !== 404 && responses.status !== 405) {
|
|
|
+ return probeResult({
|
|
|
+ protocol: 'responses',
|
|
|
+ responsesStatus: responses.status,
|
|
|
+ detail: `端点已实现 Responses API(HTTP ${responses.status})`,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isOllama) {
|
|
|
+ const version = await probeOllamaVersion(baseUrl);
|
|
|
+ if (version) {
|
|
|
+ const parsed = parseVersion(version);
|
|
|
+ if (parsed && !versionLt(parsed, OLLAMA_RESPONSES_MIN_VERSION)) {
|
|
|
+ return probeResult({
|
|
|
+ protocol: 'responses',
|
|
|
+ responsesStatus: responses.status,
|
|
|
+ ollamaVersion: version,
|
|
|
+ detail: `Ollama ${version} 支持 Responses API;/responses 返回 404 通常只是模型「${model}」未拉取,请先 ollama pull ${model}`,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return probeResult({
|
|
|
+ protocol: 'chat-only',
|
|
|
+ responsesStatus: responses.status,
|
|
|
+ ollamaVersion: version,
|
|
|
+ ollamaNeedsUpgrade: true,
|
|
|
+ detail: `Ollama ${version} 低于 0.13.3,只提供 /chat/completions(建议升级 Ollama 以直连)`,
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
- return { compatible: true, status, detail: `端点已实现 Responses API(HTTP ${status})` };
|
|
|
+
|
|
|
+ const chat = await postProbe(
|
|
|
+ `${baseUrl}/chat/completions`,
|
|
|
+ { model, messages: [{ role: 'user', content: 'ping' }], max_tokens: 1, stream: false },
|
|
|
+ params.apiKey,
|
|
|
+ );
|
|
|
+ if (chat.status !== null && chat.status !== 404 && chat.status !== 405) {
|
|
|
+ return probeResult({
|
|
|
+ protocol: 'chat-only',
|
|
|
+ responsesStatus: responses.status,
|
|
|
+ chatStatus: chat.status,
|
|
|
+ detail: `端点只提供 /chat/completions(/responses 返回 HTTP ${responses.status})`,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return probeResult({
|
|
|
+ protocol: 'unsupported',
|
|
|
+ responsesStatus: responses.status,
|
|
|
+ chatStatus: chat.status,
|
|
|
+ detail: `端点 Responses 与 Chat Completions 均不存在(HTTP ${responses.status}/${chat.status ?? '网络错误'}),无法对接`,
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
export async function readAppliedProvider(): Promise<AppliedProvider | null> {
|
|
|
@@ -193,8 +306,17 @@ export async function applyProvider(
|
|
|
// 只要显式配了地址就探(含远端 Ollama):应用成功后才发现连不上,比这里直接报错更难排查。
|
|
|
// 内置 provider 没配地址时不探(默认 localhost,可能压根没起本地服务)。
|
|
|
if (spec.baseUrl) {
|
|
|
- const probe = await probeResponsesEndpoint(spec.baseUrl, spec.apiKey);
|
|
|
- if (!probe.compatible) throw new Error(probe.detail);
|
|
|
+ const probe = await probeEndpoint({
|
|
|
+ baseUrl: spec.baseUrl,
|
|
|
+ modelId: spec.model,
|
|
|
+ apiKey: spec.apiKey,
|
|
|
+ providerType: input.providerType,
|
|
|
+ builtinProvider: spec.builtinProvider,
|
|
|
+ });
|
|
|
+ if (probe.protocol === 'chat-only') {
|
|
|
+ throw new Error(`${probe.detail}。内置桥接转换尚未启用,当前请先升级端点(Ollama 需 ≥ 0.13.3)`);
|
|
|
+ }
|
|
|
+ if (probe.protocol !== 'responses') throw new Error(probe.detail);
|
|
|
}
|
|
|
await runtime.applyProvider(spec);
|
|
|
const applied: AppliedProvider = {
|