Spaces:
Sleeping
Sleeping
| import { execFile } from "node:child_process"; | |
| import readline from "node:readline/promises"; | |
| import { stdin as input, stdout as output } from "node:process"; | |
| import { promisify } from "node:util"; | |
| const execFileAsync = promisify(execFile); | |
| const DEFAULT_OPENAI_CANDIDATES = [ | |
| "gpt-5.4", | |
| "gpt-5.3-codex" | |
| ]; | |
| const DEFAULT_ANTHROPIC_CANDIDATES = [ | |
| "claude-sonnet-4.6", | |
| "google/gemini-3-flash", | |
| ]; | |
| const DEFAULT_TIMEOUT_MS = 20_000; | |
| function joinUrl(baseUrl, path) { | |
| return `${baseUrl.replace(/\/+$/, "")}${path}`; | |
| } | |
| function sanitizeBaseUrl(baseUrl) { | |
| const trimmed = baseUrl.trim(); | |
| if (!trimmed) return trimmed; | |
| return trimmed.replace(/\/+(v1)?\/?$/, ""); | |
| } | |
| function unique(values) { | |
| return [...new Set(values.filter(Boolean))]; | |
| } | |
| async function runWithConcurrency(items, limit, worker) { | |
| const queue = [...items]; | |
| const size = Math.min(limit, queue.length); | |
| if (size <= 0) return; | |
| const workers = Array.from({ length: size }, async () => { | |
| // eslint-disable-next-line no-constant-condition | |
| while (true) { | |
| const item = queue.shift(); | |
| if (item === undefined) break; | |
| // eslint-disable-next-line no-await-in-loop | |
| await worker(item); | |
| } | |
| }); | |
| await Promise.all(workers); | |
| } | |
| async function requestJson(url, options = {}) { | |
| const startedAt = Date.now(); | |
| const method = options.method || "GET"; | |
| const headers = options.headers || {}; | |
| const body = options.body; | |
| const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; | |
| const args = ["-sS", "-i", "-m", String(Math.ceil(timeoutMs / 1000)), "-X", method, url]; | |
| for (const [key, value] of Object.entries(headers)) { | |
| args.push("-H", `${key}: ${value}`); | |
| } | |
| if (body) { | |
| args.push("--data", body); | |
| } | |
| try { | |
| const { stdout } = await execFileAsync("curl", args, { | |
| maxBuffer: 1024 * 1024 * 5, | |
| }); | |
| const normalized = stdout.replace(/\r\n/g, "\n"); | |
| const separator = normalized.lastIndexOf("\n\n"); | |
| const rawHeaders = separator >= 0 ? normalized.slice(0, separator) : ""; | |
| const text = separator >= 0 ? normalized.slice(separator + 2) : normalized; | |
| const statusLine = rawHeaders | |
| .split("\n") | |
| .filter(Boolean) | |
| .find((line) => line.startsWith("HTTP/")); | |
| const statusMatch = statusLine?.match(/^HTTP\/\S+\s+(\d+)\s*(.*)$/); | |
| const status = statusMatch ? Number(statusMatch[1]) : 0; | |
| const statusText = statusMatch?.[2] || ""; | |
| let json = null; | |
| try { | |
| json = text ? JSON.parse(text) : null; | |
| } catch { | |
| json = null; | |
| } | |
| return { | |
| ok: status >= 200 && status < 300, | |
| status, | |
| statusText, | |
| json, | |
| text, | |
| elapsedMs: Date.now() - startedAt, | |
| }; | |
| } catch (error) { | |
| const detail = [error?.stdout, error?.stderr, error?.message] | |
| .filter(Boolean) | |
| .join("\n") | |
| .trim(); | |
| return { | |
| ok: false, | |
| status: 0, | |
| statusText: "NETWORK_ERROR", | |
| json: null, | |
| text: detail || String(error), | |
| elapsedMs: Date.now() - startedAt, | |
| }; | |
| } | |
| } | |
| function anthropicHeaders(apiKey) { | |
| return { | |
| "content-type": "application/json", | |
| "x-api-key": apiKey, | |
| "anthropic-version": "2023-06-01", | |
| }; | |
| } | |
| function openaiHeaders(apiKey) { | |
| return { | |
| "content-type": "application/json", | |
| authorization: `Bearer ${apiKey}`, | |
| }; | |
| } | |
| function extractModelsFromList(payload) { | |
| if (!payload || !Array.isArray(payload.data)) return []; | |
| return payload.data | |
| .map((item) => item?.id || item?.model_name) | |
| .filter((id) => typeof id === "string" && id.length > 0); | |
| } | |
| function pickSnippet(text, max = 220) { | |
| if (!text) return ""; | |
| const normalized = text.replace(/\s+/g, " ").trim(); | |
| return normalized.length > max ? `${normalized.slice(0, max)}...` : normalized; | |
| } | |
| function hasAnthropicUsage(payload) { | |
| return Boolean(payload?.usage && typeof payload.usage.input_tokens === "number"); | |
| } | |
| function hasOpenAIOutput(payload) { | |
| if (!payload) return false; | |
| if (Array.isArray(payload.output)) return true; | |
| return typeof payload.id === "string" && typeof payload.model === "string"; | |
| } | |
| function hasChatCompletionOutput(payload) { | |
| return Array.isArray(payload?.choices) && payload.choices.length > 0; | |
| } | |
| function extractPricingModels(payload) { | |
| if (!payload || !Array.isArray(payload.data)) return []; | |
| return payload.data | |
| .map((item) => ({ | |
| model: item?.model_name, | |
| groups: Array.isArray(item?.enable_groups) ? item.enable_groups : [], | |
| endpoints: Array.isArray(item?.supported_endpoint_types) | |
| ? item.supported_endpoint_types | |
| : [], | |
| vendorId: item?.vendor_id ?? null, | |
| })) | |
| .filter((item) => item.model); | |
| } | |
| function extractPricingEndpointSummary(payload) { | |
| if (!payload || typeof payload.supported_endpoint !== "object") return {}; | |
| return payload.supported_endpoint; | |
| } | |
| function extractPricingGroups(payload) { | |
| if (!payload || typeof payload.usable_group !== "object") return {}; | |
| return payload.usable_group; | |
| } | |
| async function testOpenAIModels(baseUrl, apiKey, candidateModels) { | |
| const modelsUrl = joinUrl(baseUrl, "/v1/models"); | |
| const modelsResponse = await requestJson(modelsUrl, { | |
| method: "GET", | |
| headers: openaiHeaders(apiKey), | |
| }); | |
| const discoveredModels = extractModelsFromList(modelsResponse.json); | |
| const candidates = unique([...discoveredModels, ...candidateModels]); | |
| const reachableModels = []; | |
| const chatReachableModels = []; | |
| const failures = []; | |
| const responseProbePaths = ["/responses", "/v1/responses"]; | |
| await runWithConcurrency(candidates.slice(0, 12), 3, async (model) => { | |
| let successfulResponsesProbe = null; | |
| let lastResponsesProbe = null; | |
| for (const probePath of responseProbePaths) { | |
| // eslint-disable-next-line no-await-in-loop | |
| const responsesProbe = await requestJson(joinUrl(baseUrl, probePath), { | |
| method: "POST", | |
| headers: openaiHeaders(apiKey), | |
| body: JSON.stringify({ | |
| model, | |
| input: "Reply with OK.", | |
| max_output_tokens: 16, | |
| }), | |
| }); | |
| lastResponsesProbe = responsesProbe; | |
| if (responsesProbe.ok && hasOpenAIOutput(responsesProbe.json)) { | |
| successfulResponsesProbe = { | |
| path: probePath, | |
| elapsedMs: responsesProbe.elapsedMs, | |
| }; | |
| break; | |
| } | |
| } | |
| if (successfulResponsesProbe) { | |
| reachableModels.push({ | |
| model, | |
| kind: "responses", | |
| path: successfulResponsesProbe.path, | |
| elapsedMs: successfulResponsesProbe.elapsedMs, | |
| }); | |
| return; | |
| } | |
| const chatProbe = await requestJson(joinUrl(baseUrl, "/v1/chat/completions"), { | |
| method: "POST", | |
| headers: openaiHeaders(apiKey), | |
| body: JSON.stringify({ | |
| model, | |
| messages: [{ role: "user", content: "Reply with OK." }], | |
| max_tokens: 16, | |
| }), | |
| }); | |
| if (chatProbe.ok && hasChatCompletionOutput(chatProbe.json)) { | |
| chatReachableModels.push({ | |
| model, | |
| kind: "chat_completions", | |
| elapsedMs: chatProbe.elapsedMs, | |
| }); | |
| } else { | |
| failures.push({ | |
| model, | |
| status: chatProbe.status || lastResponsesProbe?.status || 0, | |
| responsesDetail: pickSnippet(lastResponsesProbe?.text || ""), | |
| chatDetail: pickSnippet(chatProbe.text), | |
| }); | |
| } | |
| }); | |
| return { | |
| modelsResponse, | |
| discoveredModels, | |
| reachableModels, | |
| chatReachableModels, | |
| failures, | |
| }; | |
| } | |
| async function collectPublicInfo(baseUrl) { | |
| const [statusResponse, pricingResponse] = await Promise.all([ | |
| requestJson(joinUrl(baseUrl, "/api/status"), { method: "GET" }), | |
| requestJson(joinUrl(baseUrl, "/api/pricing"), { method: "GET" }), | |
| ]); | |
| return { | |
| statusResponse, | |
| pricingResponse, | |
| pricingModels: extractPricingModels(pricingResponse.json), | |
| pricingEndpointSummary: extractPricingEndpointSummary(pricingResponse.json), | |
| usableGroups: extractPricingGroups(pricingResponse.json), | |
| }; | |
| } | |
| async function testAnthropicModels(baseUrl, apiKey, candidateModels) { | |
| const modelsUrl = joinUrl(baseUrl, "/v1/models"); | |
| const modelsResponse = await requestJson(modelsUrl, { | |
| method: "GET", | |
| headers: anthropicHeaders(apiKey), | |
| }); | |
| const discoveredModels = extractModelsFromList(modelsResponse.json); | |
| const candidates = unique([...discoveredModels, ...candidateModels]); | |
| const basicModels = []; | |
| const toolModels = []; | |
| const failures = []; | |
| await runWithConcurrency(candidates.slice(0, 12), 3, async (model) => { | |
| const basic = await requestJson(joinUrl(baseUrl, "/v1/messages"), { | |
| method: "POST", | |
| headers: anthropicHeaders(apiKey), | |
| body: JSON.stringify({ | |
| model, | |
| max_tokens: 32, | |
| messages: [{ role: "user", content: "Reply with OK." }], | |
| }), | |
| }); | |
| if (!basic.ok || !hasAnthropicUsage(basic.json)) { | |
| failures.push({ | |
| model, | |
| stage: "basic", | |
| status: basic.status, | |
| detail: pickSnippet(basic.text), | |
| }); | |
| return; | |
| } | |
| basicModels.push({ | |
| model, | |
| elapsedMs: basic.elapsedMs, | |
| }); | |
| const toolCall = await requestJson(joinUrl(baseUrl, "/v1/messages"), { | |
| method: "POST", | |
| headers: anthropicHeaders(apiKey), | |
| body: JSON.stringify({ | |
| model, | |
| max_tokens: 128, | |
| tools: [ | |
| { | |
| name: "echo", | |
| description: "Echo input text", | |
| input_schema: { | |
| type: "object", | |
| properties: { | |
| text: { type: "string" }, | |
| }, | |
| required: ["text"], | |
| }, | |
| }, | |
| ], | |
| messages: [ | |
| { | |
| role: "user", | |
| content: "Call the echo tool with text hi.", | |
| }, | |
| ], | |
| }), | |
| }); | |
| if (toolCall.ok && hasAnthropicUsage(toolCall.json)) { | |
| toolModels.push({ | |
| model, | |
| elapsedMs: toolCall.elapsedMs, | |
| }); | |
| } else { | |
| failures.push({ | |
| model, | |
| stage: "tools", | |
| status: toolCall.status, | |
| detail: pickSnippet(toolCall.text), | |
| }); | |
| } | |
| }); | |
| const countTokensResponse = await requestJson(joinUrl(baseUrl, "/v1/messages/count_tokens"), { | |
| method: "POST", | |
| headers: anthropicHeaders(apiKey), | |
| body: JSON.stringify({ | |
| model: basicModels[0]?.model || candidates[0] || "claude-3-5-sonnet-20241022", | |
| messages: [{ role: "user", content: "hi" }], | |
| }), | |
| }); | |
| return { | |
| modelsResponse, | |
| discoveredModels, | |
| basicModels, | |
| toolModels, | |
| failures, | |
| countTokensResponse, | |
| }; | |
| } | |
| function printHeader(title) { | |
| console.log(`\n=== ${title} ===`); | |
| } | |
| function printResultLine(label, value) { | |
| console.log(`${label}: ${value}`); | |
| } | |
| function formatModelList(items) { | |
| if (!items.length) return "(none)"; | |
| return items.map((item) => item.model || item).join(", "); | |
| } | |
| function maskSecret(value) { | |
| if (!value) return ""; | |
| if (value.length <= 10) return "*".repeat(value.length); | |
| return `${value.slice(0, 6)}...${value.slice(-4)}`; | |
| } | |
| function buildConfigSuggestions(baseUrl, apiKey, anthropicResult, openaiResult) { | |
| const result = { | |
| claudeCode: { | |
| supported: anthropicResult.toolModels.length > 0, | |
| reason: | |
| anthropicResult.toolModels.length > 0 | |
| ? "存在通过 tools 兼容性测试的模型" | |
| : "没有任何模型通过 Anthropic tools 兼容性测试", | |
| env: null, | |
| }, | |
| codex: { | |
| supported: openaiResult.reachableModels.length > 0, | |
| reason: | |
| openaiResult.reachableModels.length > 0 | |
| ? "存在通过 responses 接口探测的模型" | |
| : "没有任何模型通过 OpenAI responses 接口探测", | |
| configToml: null, | |
| authJson: null, | |
| }, | |
| }; | |
| if (anthropicResult.toolModels.length > 0) { | |
| const model = anthropicResult.toolModels[0].model; | |
| result.claudeCode.env = { | |
| ANTHROPIC_AUTH_TOKEN: apiKey, | |
| ANTHROPIC_BASE_URL: baseUrl, | |
| ANTHROPIC_MODEL: model, | |
| ANTHROPIC_DEFAULT_HAIKU_MODEL: model, | |
| ANTHROPIC_DEFAULT_SONNET_MODEL: model, | |
| ANTHROPIC_DEFAULT_OPUS_MODEL: model, | |
| }; | |
| } | |
| if (openaiResult.reachableModels.length > 0) { | |
| const model = openaiResult.reachableModels[0].model; | |
| result.codex.configToml = [ | |
| 'model_provider = "OpenAI"', | |
| `model = "${model}"`, | |
| `review_model = "${model}"`, | |
| "", | |
| "[model_providers.OpenAI]", | |
| 'name = "OpenAI"', | |
| `base_url = "${baseUrl}"`, | |
| 'wire_api = "responses"', | |
| "requires_openai_auth = true", | |
| `api_key = "${apiKey}"`, | |
| ].join("\n"); | |
| result.codex.authJson = { | |
| OPENAI_API_KEY: apiKey, | |
| }; | |
| } else if (openaiResult.chatReachableModels.length > 0) { | |
| const model = openaiResult.chatReachableModels[0].model; | |
| result.codex.supported = true; | |
| result.codex.reason = "存在通过 /v1/chat/completions 探测的模型"; | |
| result.codex.configToml = [ | |
| 'model_provider = "OpenAI"', | |
| `model = "${model}"`, | |
| `review_model = "${model}"`, | |
| "", | |
| "[model_providers.OpenAI]", | |
| 'name = "OpenAI"', | |
| `base_url = "${baseUrl}"`, | |
| 'wire_api = "chat_completions"', | |
| "requires_openai_auth = true", | |
| `api_key = "${apiKey}"`, | |
| ].join("\n"); | |
| result.codex.authJson = { | |
| OPENAI_API_KEY: apiKey, | |
| }; | |
| } | |
| return result; | |
| } | |
| function printJsonBlock(value) { | |
| console.log(JSON.stringify(value, null, 2)); | |
| } | |
| function printConfigSuggestions(configSuggestions) { | |
| printHeader("推荐配置"); | |
| if (configSuggestions.claudeCode.supported) { | |
| console.log("Claude Code:支持"); | |
| printJsonBlock(configSuggestions.claudeCode.env); | |
| } else { | |
| console.log("Claude Code:不建议接入这个地址"); | |
| console.log(`原因:${configSuggestions.claudeCode.reason}`); | |
| } | |
| if (configSuggestions.codex.supported) { | |
| console.log("\nCodex 的 config.toml:"); | |
| console.log(configSuggestions.codex.configToml); | |
| console.log("\nCodex 的 auth.json:"); | |
| printJsonBlock(configSuggestions.codex.authJson); | |
| } else { | |
| console.log("\nCodex:这个地址当前没有探测到可用的 OpenAI responses 模型"); | |
| console.log(`原因:${configSuggestions.codex.reason}`); | |
| } | |
| } | |
| function printModelSection(title, models) { | |
| console.log(`${title}:`); | |
| if (!models.length) { | |
| console.log("- (空)"); | |
| return; | |
| } | |
| for (const model of models) { | |
| console.log(`- ${model}`); | |
| } | |
| } | |
| function summarizeCompatibility(anthropicResult, openaiResult) { | |
| return { | |
| codex: | |
| openaiResult.reachableModels.length > 0 | |
| ? "supported-via-responses" | |
| : openaiResult.chatReachableModels.length > 0 | |
| ? "supported-via-chat-completions" | |
| : "not-confirmed", | |
| claudeCode: | |
| anthropicResult.toolModels.length > 0 ? "supported-via-anthropic-tools" : "not-confirmed", | |
| }; | |
| } | |
| function parseArgs(argv) { | |
| const parsed = {}; | |
| for (const entry of argv) { | |
| if (!entry.startsWith("--")) continue; | |
| const [rawKey, ...rest] = entry.slice(2).split("="); | |
| if (rest.length > 0) { | |
| parsed[rawKey] = rest.join("="); | |
| continue; | |
| } | |
| parsed[rawKey] = "true"; | |
| } | |
| return parsed; | |
| } | |
| function printUsage() { | |
| console.log(`Usage: | |
| npm run probe:llm -- --baseUrl=https://example.com --key=sk-xxx | |
| node src/scripts/probe-llm-endpoint.mjs --baseUrl=https://example.com --key=sk-xxx | |
| Options: | |
| --baseUrl=URL API base URL, for example https://jiuuij.de5.net | |
| --key=VALUE API key or token | |
| --help Show this help text | |
| Outputs: | |
| - 在终端输出公开信息、OpenAI / Anthropic 兼容性与推荐配置 | |
| `); | |
| } | |
| async function getInputs() { | |
| const args = parseArgs(process.argv.slice(2)); | |
| let key = args.key || args.apiKey || args.sk || args.token; | |
| let baseUrl = args.baseUrl || args.baseURL || args.url; | |
| if (key && baseUrl) { | |
| return { | |
| apiKey: key.trim(), | |
| baseUrl: sanitizeBaseUrl(baseUrl), | |
| }; | |
| } | |
| const rl = readline.createInterface({ input, output }); | |
| try { | |
| if (!baseUrl) { | |
| baseUrl = await rl.question("API base URL: "); | |
| } | |
| if (!key) { | |
| key = await rl.question("API key: "); | |
| } | |
| } finally { | |
| rl.close(); | |
| } | |
| return { | |
| apiKey: key.trim(), | |
| baseUrl: sanitizeBaseUrl(baseUrl), | |
| }; | |
| } | |
| async function main() { | |
| const args = parseArgs(process.argv.slice(2)); | |
| if (args.help === "true") { | |
| printUsage(); | |
| return; | |
| } | |
| const skipAnthropic = args.skipAnthropic === "true"; | |
| const skipOpenAI = args.skipOpenAI === "true"; | |
| const skipPublic = args.skipPublic === "true"; | |
| const { apiKey, baseUrl } = await getInputs(); | |
| if (!apiKey || !baseUrl) { | |
| console.error("Both API key and base URL are required."); | |
| process.exitCode = 1; | |
| return; | |
| } | |
| console.log(`Probing endpoint: ${baseUrl}`); | |
| const [publicInfo, anthropicResult, openaiResult] = await Promise.all([ | |
| skipPublic ? Promise.resolve({ | |
| statusResponse: { ok: false, status: 0, json: null, text: "" }, | |
| pricingResponse: { ok: false, status: 0 }, | |
| pricingModels: [], | |
| pricingEndpointSummary: {}, | |
| usableGroups: {}, | |
| }) : collectPublicInfo(baseUrl), | |
| skipAnthropic ? Promise.resolve({ | |
| modelsResponse: { ok: false, status: 0 }, | |
| discoveredModels: [], | |
| basicModels: [], | |
| toolModels: [], | |
| failures: [], | |
| countTokensResponse: { ok: false, status: 0, json: null, text: "" }, | |
| }) : testAnthropicModels(baseUrl, apiKey, DEFAULT_ANTHROPIC_CANDIDATES), | |
| skipOpenAI ? Promise.resolve({ | |
| modelsResponse: { ok: false, status: 0 }, | |
| discoveredModels: [], | |
| reachableModels: [], | |
| chatReachableModels: [], | |
| failures: [], | |
| }) : testOpenAIModels(baseUrl, apiKey, DEFAULT_OPENAI_CANDIDATES), | |
| ]); | |
| const configSuggestions = buildConfigSuggestions( | |
| baseUrl, | |
| apiKey, | |
| anthropicResult, | |
| openaiResult, | |
| ); | |
| const compatibility = summarizeCompatibility(anthropicResult, openaiResult); | |
| printHeader("探测摘要"); | |
| printResultLine("探测时间", new Date().toISOString()); | |
| printResultLine("Base URL", baseUrl); | |
| printResultLine("API Key", maskSecret(apiKey)); | |
| printResultLine("Codex 兼容性", compatibility.codex); | |
| printResultLine("Claude Code 兼容性", compatibility.claudeCode); | |
| printHeader("公开信息"); | |
| printResultLine( | |
| "公开 pricing", | |
| publicInfo.pricingResponse.ok | |
| ? `成功,共 ${publicInfo.pricingModels.length} 个模型` | |
| : `失败,状态码 ${publicInfo.pricingResponse.status}`, | |
| ); | |
| printModelSection( | |
| "公开 pricing 返回的模型", | |
| publicInfo.pricingModels.map((item) => item.model), | |
| ); | |
| printHeader("Anthropic 兼容性"); | |
| printResultLine( | |
| "模型列表接口", | |
| anthropicResult.modelsResponse.ok | |
| ? `成功,共 ${anthropicResult.discoveredModels.length} 个模型` | |
| : `失败,状态码 ${anthropicResult.modelsResponse.status}`, | |
| ); | |
| printModelSection("接口返回的模型", anthropicResult.discoveredModels); | |
| printResultLine( | |
| "基础 /v1/messages 可用模型", | |
| anthropicResult.basicModels.length > 0 | |
| ? formatModelList(anthropicResult.basicModels) | |
| : "(空)", | |
| ); | |
| printResultLine( | |
| "可用于 Claude Code 的 tools 模型", | |
| anthropicResult.toolModels.length > 0 | |
| ? formatModelList(anthropicResult.toolModels) | |
| : "(空)", | |
| ); | |
| printResultLine( | |
| "count_tokens", | |
| anthropicResult.countTokensResponse.ok | |
| ? pickSnippet(anthropicResult.countTokensResponse.text) | |
| : `失败,状态码 ${anthropicResult.countTokensResponse.status}`, | |
| ); | |
| if (anthropicResult.failures.length > 0) { | |
| console.log("最近失败记录:"); | |
| for (const failure of anthropicResult.failures.slice(0, 6)) { | |
| console.log( | |
| `- ${failure.model} [${failure.stage}] -> ${failure.status}: ${failure.detail}`, | |
| ); | |
| } | |
| } | |
| printHeader("OpenAI 兼容性"); | |
| printResultLine( | |
| "模型列表接口", | |
| openaiResult.modelsResponse.ok | |
| ? `成功,共 ${openaiResult.discoveredModels.length} 个模型` | |
| : `失败,状态码 ${openaiResult.modelsResponse.status}`, | |
| ); | |
| printModelSection("接口返回的模型", openaiResult.discoveredModels); | |
| printResultLine( | |
| "可用于 Codex 的 responses 模型", | |
| openaiResult.reachableModels.length > 0 | |
| ? openaiResult.reachableModels | |
| .map((item) => `${item.model} (${item.path})`) | |
| .join(", ") | |
| : "(空)", | |
| ); | |
| printResultLine( | |
| "可用于 Codex 的 /v1/chat/completions 模型", | |
| openaiResult.chatReachableModels.length > 0 | |
| ? formatModelList(openaiResult.chatReachableModels) | |
| : "(空)", | |
| ); | |
| if (openaiResult.failures.length > 0) { | |
| console.log("最近失败记录:"); | |
| for (const failure of openaiResult.failures.slice(0, 6)) { | |
| console.log( | |
| `- ${failure.model} -> ${failure.status}: responses=${failure.responsesDetail}; chat=${failure.chatDetail}`, | |
| ); | |
| } | |
| } | |
| // 模型汇总 | |
| printHeader("模型汇总"); | |
| printModelSection( | |
| "公开 pricing 模型", | |
| publicInfo.pricingModels.map((item) => item.model), | |
| ); | |
| printModelSection( | |
| "OpenAI 可用模型 (responses/chat_completions)", | |
| [ | |
| ...openaiResult.reachableModels.map((m) => m.model), | |
| ...openaiResult.chatReachableModels.map((m) => m.model), | |
| ], | |
| ); | |
| printModelSection( | |
| "Anthropic 可用模型 (tools)", | |
| anthropicResult.toolModels.map((m) => m.model), | |
| ); | |
| } | |
| main().catch((error) => { | |
| console.error(error); | |
| process.exitCode = 1; | |
| }); | |