Files
9router/open-sse/translator/concerns/paramSupport.js
叶炜朋 73cb89143c feat(xiaomi-mimo): merge MiMo Desktop support into xiaomi-mimo as dual auth
Adds the Desktop-exclusive Preview models and the Xiaomi account-session
route to the existing xiaomi-mimo provider instead of a separate
xiaomi-desktop provider, so the dashboard shows one MiMo entry rather than
three overlapping ones.

Dual auth, same pattern as kimi — API key (sk-) covers the cloud API,
Desktop/OAuth adds the account session used by the Preview models:

- registry: category oauth, authModes [oauth, apikey], oauth block, the two
  mimo-x-*-preview models, and the invite signupUrl
- executor: routes Preview models to the account-service route with a Cookie
  session, everything else keeps the sourceFormat-matched transport
- oauth: custom ECDH encrypted-callback flow (X25519 -> SHA256 -> AES-256-GCM)
  with a loopback callback proxy, plus one-click import of the local Desktop
  auth.json
- usage: weekly quota from the account session

Fixes found while merging:

- the OAuth browser flow was dead: poll-status cleared the session before the
  client could POST /exchange, so every exchange returned 400
- a Claude-format client was sent to /v1/chat/completions instead of the
  declared /anthropic/v1/messages transport, because buildUrl ignored
  runtimeTransport
- stopXiaomiMimoProxy leaked every pending session (each holding an X25519
  private key) for the process lifetime
- the OAuth exchange did not persist the Desktop passToken, so the Preview
  models could never work after a browser sign-in

Removes dead code: the local engine token minting (mimoEngine, never called
on the request path), the model-catalog and usage routes, engineToken/
engineUrl plumbing, and an unread top-level usage block.

Adds tests/unit/xiaomi-mimo-{executor,oauth-session,oauth-proxy}.test.js —
the provider previously had none.
2026-09-10 23:42:41 +07:00

79 lines
3.8 KiB
JavaScript

import { getCapabilitiesForModel } from "../../providers/capabilities.js";
// Strip request params a given provider/model rejects upstream (e.g. HTTP 400).
// Config-driven: add a rule instead of scattering `delete body.x` across executors.
// Each rule: optional provider, regex match on model, list of params to drop.
// A param is removed only when it is present (!== undefined).
const STRIP_RULES = [
// All Claude models: temperature deprecated/rejected upstream (Anthropic 400). #1748
{ match: /claude/i, drop: ["temperature"] },
// GitHub Copilot gpt-5.4: temperature unsupported.
{ provider: "github", match: /gpt-5\.4/i, drop: ["temperature"] },
// GitHub Copilot Claude (except opus/sonnet 4.6): thinking + reasoning_effort rejected. #713
{ provider: "github", match: (m) => /claude/i.test(m) && !/claude.*(opus|sonnet).*4\.6/i.test(m), drop: ["thinking", "reasoning_effort"] },
// Cloudflare Workers AI: content must be plain string, rejects OpenAI content-part array (#1926)
{ provider: "cloudflare-ai", flattenContent: true },
// MiMo Desktop Preview models (account-service route): content must be plain string,
// rejects OpenAI content-part array. Cloud models keep their parts (mimo-v2-omni is multi-modal).
{ provider: "xiaomi-mimo", match: /preview/i, flattenContent: true },
{ provider: "volcengine-ark", match: /glm-5/i, clampToModelMaxOutput: true },
// VolcEngine Ark caps the Kimi family at max_tokens <= 32768, but the model's
// advertised ceiling is far higher (Kimi-K2.7-Code resolves to maxOutput 262144),
// so clampToModelMaxOutput alone leaves it uncapped and the request 400s with
// "integer above maximum value, expected <= 32768". Pin an explicit endpoint cap;
// min() with the model ceiling still applies if a variant's own limit is lower.
{ provider: "volcengine-ark", match: /kimi/i, maxOutputCap: 32768, clampToModelMaxOutput: true },
];
// Test a rule's match (regex or predicate) against the model id.
function matches(rule, model) {
if (!rule.match) return true;
return typeof rule.match === "function" ? rule.match(model) : rule.match.test(model);
}
function clampNumber(body, key, ceiling) {
if (typeof body[key] === "number" && Number.isFinite(body[key]) && body[key] > ceiling) {
body[key] = ceiling;
}
}
// Remove unsupported params from body in place; returns body.
export function stripUnsupportedParams(provider, model, body) {
if (!model || !body || typeof body !== "object") return body;
for (const rule of STRIP_RULES) {
if (rule.provider && rule.provider !== provider) continue;
if (!matches(rule, model)) continue;
for (const key of rule.drop || []) {
if (body[key] !== undefined) delete body[key];
}
// CF Workers AI oneOf root schema only accepts content as plain string (#1926)
if (rule.flattenContent && Array.isArray(body.messages)) {
for (const msg of body.messages) {
if (msg && Array.isArray(msg.content)) {
msg.content = msg.content
.map(b => (b?.type === "text" && typeof b.text === "string") ? b.text : "")
.join("");
}
}
}
if (rule.clampToModelMaxOutput || Number.isFinite(rule.maxOutputCap)) {
const modelCeiling = getCapabilitiesForModel(provider, model).maxOutput;
const candidates = [];
if (rule.clampToModelMaxOutput && Number.isFinite(modelCeiling) && modelCeiling > 0) {
candidates.push(modelCeiling);
}
if (Number.isFinite(rule.maxOutputCap) && rule.maxOutputCap > 0) {
candidates.push(rule.maxOutputCap);
}
if (candidates.length > 0) {
const ceiling = Math.min(...candidates);
clampNumber(body, "max_tokens", ceiling);
clampNumber(body, "max_completion_tokens", ceiling);
clampNumber(body, "max_output_tokens", ceiling);
}
}
}
return body;
}