




















@@ -1,5 +1,9 @@
11import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
22import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
3+import {
4+fetchWithSsrFGuard,
5+ssrfPolicyFromHttpBaseUrlAllowedHostname,
6+} from "openclaw/plugin-sdk/ssrf-runtime";
37import {
48normalizeLowercaseStringOrEmpty,
59normalizeOptionalString,
@@ -533,77 +537,94 @@ export async function discoverChutesModels(accessToken?: string): Promise<ModelD
533537}
534538535539try {
536-let response = await fetch(`${CHUTES_BASE_URL}/models`, {
537-signal: AbortSignal.timeout(10_000),
538- headers,
540+let guardedFetch = await fetchWithSsrFGuard({
541+url: `${CHUTES_BASE_URL}/models`,
542+init: {
543+signal: AbortSignal.timeout(10_000),
544+ headers,
545+},
546+policy: ssrfPolicyFromHttpBaseUrlAllowedHostname(CHUTES_BASE_URL),
547+auditContext: "chutes-model-discovery",
539548});
549+let response = guardedFetch.response;
540550541551if (response.status === 401 && trimmedKey) {
552+await guardedFetch.release();
542553effectiveKey = "";
543-response = await fetch(`${CHUTES_BASE_URL}/models`, {
544-signal: AbortSignal.timeout(10_000),
554+guardedFetch = await fetchWithSsrFGuard({
555+url: `${CHUTES_BASE_URL}/models`,
556+init: {
557+signal: AbortSignal.timeout(10_000),
558+},
559+policy: ssrfPolicyFromHttpBaseUrlAllowedHostname(CHUTES_BASE_URL),
560+auditContext: "chutes-model-discovery",
545561});
562+response = guardedFetch.response;
546563}
547564548-if (!response.ok) {
549-if (response.status !== 401 && response.status !== 503) {
550-log.warn(`GET /v1/models failed: HTTP ${response.status}, using static catalog`);
565+try {
566+if (!response.ok) {
567+if (response.status !== 401 && response.status !== 503) {
568+log.warn(`GET /v1/models failed: HTTP ${response.status}, using static catalog`);
569+}
570+return staticCatalog();
551571}
552-return staticCatalog();
553-}
554572555-const body = (await response.json()) as OpenAIListModelsResponse;
556-const data = body?.data;
557-if (!Array.isArray(data) || data.length === 0) {
558-log.warn("No models in response, using static catalog");
559-return staticCatalog();
560-}
573+ const body = (await response.json()) as OpenAIListModelsResponse;
574+ const data = body?.data;
575+ if (!Array.isArray(data) || data.length === 0) {
576+ log.warn("No models in response, using static catalog");
577+ return staticCatalog();
578+ }
561579562-const seen = new Set<string>();
563-const models: ModelDefinitionConfig[] = [];
580+ const seen = new Set<string>();
581+ const models: ModelDefinitionConfig[] = [];
564582565-for (const entry of data) {
566-const id = normalizeOptionalString(entry?.id) ?? "";
567-if (!id || seen.has(id)) {
568-continue;
569-}
570-seen.add(id);
583+ for (const entry of data) {
584+ const id = normalizeOptionalString(entry?.id) ?? "";
585+ if (!id || seen.has(id)) {
586+ continue;
587+ }
588+ seen.add(id);
571589572-const lowerId = normalizeLowercaseStringOrEmpty(id);
573-const isReasoning =
574-entry.supported_features?.includes("reasoning") ||
575-lowerId.includes("r1") ||
576-lowerId.includes("thinking") ||
577-lowerId.includes("reason") ||
578-lowerId.includes("tee");
590+ const lowerId = normalizeLowercaseStringOrEmpty(id);
591+ const isReasoning =
592+ entry.supported_features?.includes("reasoning") ||
593+ lowerId.includes("r1") ||
594+ lowerId.includes("thinking") ||
595+ lowerId.includes("reason") ||
596+ lowerId.includes("tee");
579597580-const input: Array<"text" | "image"> = (entry.input_modalities || ["text"]).filter(
581-(i): i is "text" | "image" => i === "text" || i === "image",
582-);
598+ const input: Array<"text" | "image"> = (entry.input_modalities || ["text"]).filter(
599+ (i): i is "text" | "image" => i === "text" || i === "image",
600+ );
583601584-models.push({
585- id,
586-name: id,
587-reasoning: isReasoning,
588- input,
589-cost: {
590-input: entry.pricing?.prompt || 0,
591-output: entry.pricing?.completion || 0,
592-cacheRead: 0,
593-cacheWrite: 0,
594-},
595-contextWindow: entry.context_length || CHUTES_DEFAULT_CONTEXT_WINDOW,
596-maxTokens: entry.max_output_length || CHUTES_DEFAULT_MAX_TOKENS,
597-compat: {
598-supportsUsageInStreaming: false,
599-},
600-});
601-}
602+ models.push({
603+ id,
604+ name: id,
605+ reasoning: isReasoning,
606+ input,
607+ cost: {
608+ input: entry.pricing?.prompt || 0,
609+ output: entry.pricing?.completion || 0,
610+ cacheRead: 0,
611+ cacheWrite: 0,
612+ },
613+ contextWindow: entry.context_length || CHUTES_DEFAULT_CONTEXT_WINDOW,
614+ maxTokens: entry.max_output_length || CHUTES_DEFAULT_MAX_TOKENS,
615+ compat: {
616+ supportsUsageInStreaming: false,
617+ },
618+ });
619+ }
602620603-return cacheAndReturn(
604-effectiveKey,
605-models.length > 0 ? models : CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition),
606-);
621+return cacheAndReturn(
622+effectiveKey,
623+models.length > 0 ? models : CHUTES_MODEL_CATALOG.map(buildChutesModelDefinition),
624+);
625+} finally {
626+await guardedFetch.release();
627+}
607628} catch (error) {
608629log.warn(`Discovery failed: ${String(error)}, using static catalog`);
609630return staticCatalog();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。