Download server/src/providers/modelscope.ts from automindy/fokemal: direct link, hf CLI and curl.
- Browser
- Download file 7.32 kB
-
https://huggingface.co/spaces/automindy/fokemal/resolve/main/server/src/providers/modelscope.ts
- Command line
-
hf download hf://spaces/automindy/fokemal/server/src/providers/modelscope.ts
-
curl -L -o modelscope.ts https://huggingface.co/spaces/automindy/fokemal/resolve/main/server/src/providers/modelscope.ts
7.32 kB
| import { createHash } from 'node:crypto'; | |
| import { OpenAICompatProvider } from './openai-compat.js'; | |
| import type { KeyValidationResult } from './base.js'; | |
| import { recordQuotaObservationsFromResponse, type QuotaObservationContext } from '../services/provider-quota.js'; | |
| const MODELSCOPE_BASE_URL = 'https://api-inference.modelscope.cn/v1'; | |
| // A successful validation is cached per key for this long. The 5-minute | |
| // health pass would otherwise burn ~288 paid 1-token completions per key | |
| // per day against the account's magic-grain (ιη²) quota β observed as | |
| // 2 ιη² per ultra-tier request (2026-08-14). A revoked key is still caught | |
| // by the next real request's 401 handling (error-classify disables it), so | |
| // the cache only delays proactive detection, never hides a live failure. | |
| const DEFAULT_VALIDATE_CACHE_MS = 24 * 60 * 60 * 1000; | |
| function modelscopeValidateCacheMs(): number { | |
| const raw = process.env.MODELSCOPE_VALIDATE_CACHE_MS; | |
| if (raw !== undefined && raw.trim() !== '') { | |
| const n = Number(raw); | |
| if (Number.isInteger(n) && n >= 0) return n; | |
| } | |
| return DEFAULT_VALIDATE_CACHE_MS; | |
| } | |
| /** | |
| * ModelScope (ιζη€ΎεΊ, Alibaba) β OpenAI-compatible inference API. | |
| * | |
| * Everything except key validation is stock OpenAI-compat. validateKey is | |
| * overridden because of a trap in ModelScope's API surface: | |
| * | |
| * GET /v1/models returns 200 WITHOUT auth β and, verified keyless on | |
| * 2026-07-26, it also returns 200 with a GARBAGE Bearer token. The default | |
| * openai-compat validateKey (GET /v1/models with the key) would therefore | |
| * mark any invalid key healthy forever. /v1/models is useless for | |
| * validation on this platform. | |
| * | |
| * The only endpoint verified to enforce auth is POST /v1/chat/completions: | |
| * a garbage token gets a clean | |
| * 401 {"error":{"message":"Authentication failed, please make sure that a | |
| * valid ModelScope token is supplied."}} | |
| * before any generation happens. So validation makes a 1-token chat | |
| * completion. The model id is picked dynamically from GET /v1/models (first | |
| * entry) because the roster churns β a hardcoded id would rot. | |
| * | |
| * COST: a successful validation burns 1 paid 1-token completion per health | |
| * check against the account's magic-grain (ιη²) quota β observed as 2 ιη² | |
| * per ultra-tier request (2026-08-14), not the historical 2000-requests/day | |
| * free API quota. With the default 5-minute health pass that would be ~288 | |
| * paid probes per key per day. A successful validation is therefore cached | |
| * per key for MODELSCOPE_VALIDATE_CACHE_MS (default 24h) β repeat health | |
| * passes return true without re-probing, taking the steady-state cost to one | |
| * probe per key per day. A failed-auth validation is rejected before | |
| * generation and, per maintainer reports, does not count against quota. | |
| * | |
| * Community testers must confirm (we have NO real token for this platform): | |
| * the maintainer-documented bad-binding failure is | |
| * `401 please bind your alibaba cloud account before use` | |
| * β a token minted without binding the ModelScope account to an Alibaba | |
| * Cloud CHINA-site account fails every call with that message. It flows | |
| * through validationResult() into the health error verbatim so users see the | |
| * actionable reason instead of a generic "invalid key". See issue #581. | |
| */ | |
| export class ModelScopeProvider extends OpenAICompatProvider { | |
| /** key-material fingerprint β last successful validation wall-clock (ms). | |
| * Keyed on the token itself, not the row id, so editing a key in place | |
| * re-probes instead of inheriting the old token's verdict. */ | |
| private readonly lastValidatedAt = new Map<string, number>(); | |
| constructor() { | |
| super({ | |
| platform: 'modelscope', | |
| name: 'ModelScope', | |
| // Serves large reasoning models (DeepSeek-V4, Qwen3 Thinking variants) | |
| // from cn-region infrastructure; cross-region TTFB plus buffered | |
| // reasoning phases blow the 60s default. 90s matches NVIDIA NIM's | |
| // allowance for the same model class. | |
| baseUrl: MODELSCOPE_BASE_URL, | |
| timeoutMs: 90_000, | |
| }); | |
| } | |
| override async validateKey(apiKey: string, quotaContext?: QuotaObservationContext): Promise<KeyValidationResult> { | |
| // Validation cache: a key validated successfully within | |
| // MODELSCOPE_VALIDATE_CACHE_MS is trusted without re-probing. The | |
| // 5-minute health pass would otherwise burn ~288 paid 1-token | |
| // completions per key per day against the magic-grain quota. A revoked | |
| // key is still caught by the next real request's 401 handling, so this | |
| // only delays proactive detection. | |
| const cacheMs = modelscopeValidateCacheMs(); | |
| const fingerprint = createHash('sha256').update(apiKey).digest('hex'); | |
| if (cacheMs > 0) { | |
| const last = this.lastValidatedAt.get(fingerprint); | |
| if (last !== undefined && Date.now() - last < cacheMs) { | |
| return true; | |
| } | |
| } | |
| // Transport errors (DNS / timeout / TLS) propagate β health.ts marks | |
| // status='error' without counting toward auto-disable; only a confirmed | |
| // 401/403 disables a key. | |
| // Step 1: pick a live model id for the auth probe. This call is NOT the | |
| // validation (it answers 200 for any token, see class comment) β it only | |
| // keeps the probe's model id in sync with the churning roster. | |
| const modelsRes = await this.fetchWithTimeout(`${MODELSCOPE_BASE_URL}/models`, { | |
| method: 'GET', | |
| headers: { 'Authorization': `Bearer ${apiKey}` }, | |
| }, 30000, { timeoutBounds: 'request' }); | |
| if (!modelsRes.ok) { | |
| // Auth failures cannot happen here (endpoint is unauthenticated); treat | |
| // any non-200 as an upstream outage β status='error', never 'invalid'. | |
| throw new Error(`ModelScope /models returned HTTP ${modelsRes.status} while picking a validation probe model`); | |
| } | |
| const roster = await modelsRes.json().catch(() => null) as { data?: Array<{ id?: string }> } | null; | |
| const probeModel = roster?.data?.[0]?.id; | |
| if (!probeModel) { | |
| throw new Error('ModelScope /models returned no models to probe key validity against'); | |
| } | |
| // Step 2: the actual auth check β a minimal 1-token completion. | |
| const res = await this.fetchWithTimeout(`${MODELSCOPE_BASE_URL}/chat/completions`, { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Bearer ${apiKey}`, | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| model: probeModel, | |
| messages: [{ role: 'user', content: 'ping' }], | |
| max_tokens: 1, | |
| }), | |
| }, 30000, { timeoutBounds: 'request' }); | |
| recordQuotaObservationsFromResponse(res, { | |
| platform: this.platform, | |
| keyId: quotaContext?.keyId, | |
| providerAccountId: quotaContext?.providerAccountId, | |
| modelId: probeModel, | |
| quotaPoolKey: quotaContext?.quotaPoolKey, | |
| endpoint: 'chat/completions', | |
| }); | |
| // validationResult: 401/403 β {valid:false, error:<upstream message>} | |
| // (surfaces the alibaba-binding reason); everything else β 200, but also | |
| // 400/404 (probe model quirk), 429 (quota) β proves the token | |
| // authenticated and returns true. | |
| const result = await this.validationResult(res); | |
| if (result === true && cacheMs > 0) { | |
| this.lastValidatedAt.set(fingerprint, Date.now()); | |
| } | |
| return result; | |
| } | |
| } | |