feat(open-sse): add schema coercion and tool sanitization

- introduce open-sse/translator/helpers/schemaCoercion.ts to coerce
  numeric JSON Schema fields encoded as strings
- wire coerceToolSchemas and sanitizeToolDescriptions into translator
  pipeline; ensure tool descriptions are sanitized
- inject empty reasoning content for tool calls when target is OpenAI
  format
- update qwen base URL to DashScope-compatible endpoint
- extend antigravity static catalog with Gemini 3.1 pro preview models and
  update Gemini model specs with preview aliases
- implement call log max cap caching with TTL; expose invalidateCallLogsMaxCache
  and invalidate on settings PATCH
- add tests: call-log-cap.test.mjs and tool-request-sanitization.test.mjs;
  extend tests for Windsurf integration and gemini previews
- update CLI runtime and tools to include Windsurf as a guide-only tool
- add maxCallLogs to validation schemas (settings and updateSettings)
- add Czech README (README.cs.md) to repository
This commit is contained in:
diegosouzapw
2026-03-28 12:33:13 -03:00
parent cfb390936a
commit 55325773dc
21 changed files with 2825 additions and 9 deletions

1
.gitignore vendored
View File

@@ -112,6 +112,7 @@ app.log
# Backup directories
app.__qa_backup/
.app-build-backup-*/
# Production standalone build (created by scripts/prepublish.mjs)
# Conflicts with Next.js App Router detection in dev (root app/ shadows src/app/)

2081
README.cs.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -291,7 +291,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
alias: "qw",
format: "openai",
executor: "default",
baseUrl: "https://portal.qwen.ai/v1/chat/completions",
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions",
authType: "oauth",
authHeader: "bearer",
headers: {

View File

@@ -0,0 +1,207 @@
/**
* Shared sanitizers for tool payloads that arrive from IDEs/SDKs with
* JSON Schema numeric constraints encoded as strings or invalid descriptions.
*/
type JsonRecord = Record<string, unknown>;
const NUMERIC_SCHEMA_FIELDS = [
"minimum",
"maximum",
"exclusiveMinimum",
"exclusiveMaximum",
"minLength",
"maxLength",
"minItems",
"maxItems",
"minProperties",
"maxProperties",
"multipleOf",
] as const;
function isPlainObject(value: unknown): value is JsonRecord {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function coerceNumericString(value: unknown): unknown {
if (typeof value !== "string") return value;
const trimmed = value.trim();
if (trimmed.length === 0) return value;
const parsed = Number(trimmed);
return Number.isFinite(parsed) ? parsed : value;
}
function mapRecordValues(record: JsonRecord): JsonRecord {
return Object.fromEntries(
Object.entries(record).map(([key, value]) => [key, coerceSchemaNumericFields(value)])
);
}
function sanitizeDescriptionValue(value: unknown): string | undefined {
if (value === undefined) return undefined;
if (value === null) return "";
return typeof value === "string" ? value : String(value);
}
export function coerceSchemaNumericFields(schema: unknown): unknown {
if (Array.isArray(schema)) {
return schema.map((entry) => coerceSchemaNumericFields(entry));
}
if (!isPlainObject(schema)) return schema;
const result: JsonRecord = { ...schema };
for (const field of NUMERIC_SCHEMA_FIELDS) {
if (field in result) {
result[field] = coerceNumericString(result[field]);
}
}
if (isPlainObject(result.properties)) {
result.properties = mapRecordValues(result.properties);
}
if (isPlainObject(result.patternProperties)) {
result.patternProperties = mapRecordValues(result.patternProperties);
}
if (isPlainObject(result.definitions)) {
result.definitions = mapRecordValues(result.definitions);
}
if (isPlainObject(result.$defs)) {
result.$defs = mapRecordValues(result.$defs);
}
if (isPlainObject(result.dependentSchemas)) {
result.dependentSchemas = mapRecordValues(result.dependentSchemas);
}
if (result.items !== undefined) {
result.items = coerceSchemaNumericFields(result.items);
}
if (result.additionalProperties && typeof result.additionalProperties === "object") {
result.additionalProperties = coerceSchemaNumericFields(result.additionalProperties);
}
if (result.unevaluatedProperties && typeof result.unevaluatedProperties === "object") {
result.unevaluatedProperties = coerceSchemaNumericFields(result.unevaluatedProperties);
}
if (Array.isArray(result.prefixItems)) {
result.prefixItems = result.prefixItems.map((entry) => coerceSchemaNumericFields(entry));
}
if (Array.isArray(result.anyOf)) {
result.anyOf = result.anyOf.map((entry) => coerceSchemaNumericFields(entry));
}
if (Array.isArray(result.oneOf)) {
result.oneOf = result.oneOf.map((entry) => coerceSchemaNumericFields(entry));
}
if (Array.isArray(result.allOf)) {
result.allOf = result.allOf.map((entry) => coerceSchemaNumericFields(entry));
}
if (isPlainObject(result.not)) {
result.not = coerceSchemaNumericFields(result.not);
}
if (isPlainObject(result.if)) {
result.if = coerceSchemaNumericFields(result.if);
}
if (isPlainObject(result.then)) {
result.then = coerceSchemaNumericFields(result.then);
}
if (isPlainObject(result.else)) {
result.else = coerceSchemaNumericFields(result.else);
}
return result;
}
export function sanitizeToolDescription(tool: unknown): unknown {
if (!isPlainObject(tool)) return tool;
const result: JsonRecord = { ...tool };
if (isPlainObject(result.function) && "description" in result.function) {
const description = sanitizeDescriptionValue(result.function.description);
if (description !== undefined) {
result.function = { ...result.function, description };
}
}
if (!isPlainObject(result.function) && "description" in result) {
const description = sanitizeDescriptionValue(result.description);
if (description !== undefined) {
result.description = description;
}
}
if (Array.isArray(result.functionDeclarations)) {
result.functionDeclarations = result.functionDeclarations.map((declaration) => {
if (!isPlainObject(declaration) || !("description" in declaration)) return declaration;
const description = sanitizeDescriptionValue(declaration.description);
return description === undefined ? declaration : { ...declaration, description };
});
}
return result;
}
export function coerceToolSchemas(tools: unknown): unknown {
if (!Array.isArray(tools)) return tools;
return tools.map((tool) => {
if (!isPlainObject(tool)) return tool;
const result: JsonRecord = { ...tool };
if (isPlainObject(result.function) && "parameters" in result.function) {
result.function = {
...result.function,
parameters: coerceSchemaNumericFields(result.function.parameters),
};
}
if (result.input_schema !== undefined) {
result.input_schema = coerceSchemaNumericFields(result.input_schema);
}
if ("parameters" in result && !isPlainObject(result.function)) {
result.parameters = coerceSchemaNumericFields(result.parameters);
}
if (Array.isArray(result.functionDeclarations)) {
result.functionDeclarations = result.functionDeclarations.map((declaration) => {
if (!isPlainObject(declaration) || !("parameters" in declaration)) return declaration;
return {
...declaration,
parameters: coerceSchemaNumericFields(declaration.parameters),
};
});
}
return result;
});
}
export function sanitizeToolDescriptions(tools: unknown): unknown {
if (!Array.isArray(tools)) return tools;
return tools.map((tool) => sanitizeToolDescription(tool));
}
export function injectEmptyReasoningContentForToolCalls(
messages: unknown,
provider: unknown
): unknown {
if (!Array.isArray(messages) || String(provider || "").toLowerCase() !== "deepseek") {
return messages;
}
return messages.map((message) => {
if (!isPlainObject(message)) return message;
if (
message.role !== "assistant" ||
!Array.isArray(message.tool_calls) ||
message.tool_calls.length === 0 ||
message.reasoning_content !== undefined
) {
return message;
}
return { ...message, reasoning_content: "" };
});
}

View File

@@ -2,6 +2,11 @@ import { FORMATS } from "./formats.ts";
import { ensureToolCallIds, fixMissingToolResponses } from "./helpers/toolCallHelper.ts";
import { prepareClaudeRequest } from "./helpers/claudeHelper.ts";
import { filterToOpenAIFormat } from "./helpers/openaiHelper.ts";
import {
coerceToolSchemas,
injectEmptyReasoningContentForToolCalls,
sanitizeToolDescriptions,
} from "./helpers/schemaCoercion.ts";
import { getRequestTranslator, getResponseTranslator } from "./registry.ts";
import { bootstrapTranslatorRegistry } from "./bootstrap.ts";
import { normalizeThinkingConfig } from "../services/provider.ts";
@@ -171,6 +176,15 @@ export function translateRequest(
);
}
if (result.tools !== undefined) {
result.tools = coerceToolSchemas(result.tools);
result.tools = sanitizeToolDescriptions(result.tools);
}
if (targetFormat === FORMATS.OPENAI && result.messages && Array.isArray(result.messages)) {
result.messages = injectEmptyReasoningContentForToolCalls(result.messages, provider);
}
// Ensure unique tool_call ids on final payload (translators may have introduced duplicates)
ensureToolCallIds(result, { use9CharId });
fixMissingToolResponses(result);

View File

@@ -18,6 +18,11 @@ export default function SystemStorageTab() {
const [importStatus, setImportStatus] = useState({ type: "", message: "" });
const [confirmImport, setConfirmImport] = useState(false);
const [pendingImportFile, setPendingImportFile] = useState<File | null>(null);
const [maxCallLogs, setMaxCallLogs] = useState(10000);
const [maxCallLogsDraft, setMaxCallLogsDraft] = useState("10000");
const [settingsLoading, setSettingsLoading] = useState(true);
const [maxCallLogsSaving, setMaxCallLogsSaving] = useState(false);
const [maxCallLogsStatus, setMaxCallLogsStatus] = useState({ type: "", message: "" });
const fileInputRef = useRef<HTMLInputElement>(null);
const locale = useLocale();
const t = useTranslations("settings");
@@ -54,6 +59,27 @@ export default function SystemStorageTab() {
}
};
const loadSettings = async () => {
setSettingsLoading(true);
try {
const res = await fetch("/api/settings");
if (!res.ok) return;
const data = await res.json();
const value =
typeof data.maxCallLogs === "number" &&
Number.isInteger(data.maxCallLogs) &&
data.maxCallLogs > 0
? data.maxCallLogs
: 10000;
setMaxCallLogs(value);
setMaxCallLogsDraft(String(value));
} catch (err) {
console.error("Failed to fetch settings:", err);
} finally {
setSettingsLoading(false);
}
};
const handleManualBackup = async () => {
setManualBackupLoading(true);
setManualBackupStatus({ type: "", message: "" });
@@ -119,8 +145,47 @@ export default function SystemStorageTab() {
useEffect(() => {
loadStorageHealth();
loadSettings();
}, []);
const handleSaveMaxCallLogs = async () => {
const parsed = Number.parseInt(maxCallLogsDraft, 10);
if (!Number.isInteger(parsed) || parsed <= 0) {
setMaxCallLogsStatus({
type: "error",
message: "Enter a positive integer for the call log limit.",
});
return;
}
setMaxCallLogsSaving(true);
setMaxCallLogsStatus({ type: "", message: "" });
try {
const res = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ maxCallLogs: parsed }),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Failed to save call log limit");
}
setMaxCallLogs(parsed);
setMaxCallLogsDraft(String(parsed));
setMaxCallLogsStatus({
type: "success",
message: "Call log retention limit saved.",
});
} catch (err) {
setMaxCallLogsStatus({
type: "error",
message: (err as Error).message || "Failed to save call log limit",
});
} finally {
setMaxCallLogsSaving(false);
}
};
const handleExport = async () => {
setExportLoading(true);
try {
@@ -276,6 +341,56 @@ export default function SystemStorageTab() {
</div>
</div>
<div className="p-3 rounded-lg bg-bg border border-border mb-4">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-medium text-text-main">Call log retention limit</p>
<p className="text-xs text-text-muted">
Keep only the most recent call log entries in SQLite. Older entries are pruned
automatically after each new request log is saved.
</p>
</div>
<Badge variant="default" size="sm">
{maxCallLogs.toLocaleString()}
</Badge>
</div>
<div className="flex flex-wrap items-center gap-2 mt-3">
<input
type="number"
min="1"
step="1"
value={maxCallLogsDraft}
onChange={(e) => setMaxCallLogsDraft(e.target.value)}
disabled={settingsLoading || maxCallLogsSaving}
className="w-40 rounded-lg border border-border bg-bg-secondary px-3 py-2 text-sm text-text-main focus:outline-none focus:ring-1 focus:ring-primary/40"
aria-label="Call log retention limit"
/>
<Button
variant="outline"
size="sm"
onClick={handleSaveMaxCallLogs}
loading={maxCallLogsSaving}
disabled={settingsLoading}
>
Save limit
</Button>
</div>
{maxCallLogsStatus.message && (
<div
className={`mt-3 rounded-lg border px-3 py-2 text-sm ${
maxCallLogsStatus.type === "success"
? "border-green-500/20 bg-green-500/10 text-green-500"
: "border-red-500/20 bg-red-500/10 text-red-500"
}`}
role="alert"
>
{maxCallLogsStatus.message}
</div>
)}
</div>
{/* Export / Import */}
<div className="flex flex-wrap items-center gap-2 mb-4">
<Button variant="outline" size="sm" onClick={handleExport} loading={exportLoading}>

View File

@@ -59,6 +59,8 @@ const STATIC_MODEL_PROVIDERS: Record<string, () => Array<{ id: string; name: str
antigravity: () => [
{ id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 Thinking" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview" },
{ id: "gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview" },
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro" },
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" },
{ id: "gemini-2.0-flash", name: "Gemini 2.0 Flash" },
@@ -141,7 +143,7 @@ const PROVIDER_MODELS_CONFIG: Record<string, ProviderModelsConfigEntry> = {
})),
},
qwen: {
url: "https://portal.qwen.ai/v1/models",
url: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models",
method: "GET",
headers: { "Content-Type": "application/json" },
authHeader: "Authorization",

View File

@@ -59,8 +59,8 @@ const OAUTH_TEST_CONFIG = {
refreshable: true,
},
qwen: {
// portal.qwen.ai/v1/models returns 404 — endpoint no longer exists.
// Use checkExpiry instead — actual connectivity is validated via real requests.
// Qwen OAuth is validated via token expiry; model import uses DashScope /models.
// Use checkExpiry here — actual request connectivity is validated via real calls.
checkExpiry: true,
refreshable: true,
},

View File

@@ -114,6 +114,11 @@ export async function PATCH(request) {
setCliCompatProviders(body.cliCompatProviders || []);
}
if ("maxCallLogs" in body) {
const { invalidateCallLogsMaxCache } = await import("@/lib/usage/callLogs");
invalidateCallLogsMaxCache();
}
const { password, ...safeSettings } = settings;
return NextResponse.json(safeSettings);
} catch (error) {

View File

@@ -48,7 +48,8 @@ function hasTruncatedFlag(value: unknown): boolean {
return (value as Record<string, unknown>)._truncated === true;
}
const CALL_LOGS_MAX = parseInt(process.env.CALL_LOGS_MAX || "200", 10);
const DEFAULT_CALL_LOGS_MAX = 10_000;
const CALL_LOGS_MAX_CACHE_TTL_MS = 30_000;
const LOG_RETENTION_DAYS = parseInt(process.env.LOG_RETENTION_DAYS || "7", 10);
const CALL_LOG_PAYLOAD_MODE = (() => {
const value = (process.env.CALL_LOG_PAYLOAD_MODE || "full").toLowerCase();
@@ -57,6 +58,55 @@ const CALL_LOG_PAYLOAD_MODE = (() => {
const shouldLogPayloadInDb = CALL_LOG_PAYLOAD_MODE !== "none";
const shouldLogPayloadOnDisk = CALL_LOG_PAYLOAD_MODE === "full";
let callLogsMaxCache = {
value: resolveCallLogsMaxValue(process.env.CALL_LOGS_MAX) ?? DEFAULT_CALL_LOGS_MAX,
expiresAt: 0,
};
function resolveCallLogsMaxValue(value: unknown): number | null {
if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
if (typeof value === "string" && value.trim().length > 0) {
const parsed = Number.parseInt(value, 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
}
return null;
}
async function getMaxCallLogs(): Promise<number> {
const now = Date.now();
if (callLogsMaxCache.expiresAt > now) {
return callLogsMaxCache.value;
}
let value = resolveCallLogsMaxValue(process.env.CALL_LOGS_MAX) ?? DEFAULT_CALL_LOGS_MAX;
try {
const { getSettings } = await import("@/lib/localDb");
const settings = await getSettings();
const configured =
resolveCallLogsMaxValue(settings.maxCallLogs) ??
resolveCallLogsMaxValue(settings.MAX_CALL_LOGS);
if (configured !== null) {
value = configured;
}
} catch {
// Fall back to env/default cap when settings are unavailable.
}
callLogsMaxCache = {
value,
expiresAt: now + CALL_LOGS_MAX_CACHE_TTL_MS,
};
return value;
}
export function invalidateCallLogsMaxCache(): void {
callLogsMaxCache = {
value: resolveCallLogsMaxValue(process.env.CALL_LOGS_MAX) ?? DEFAULT_CALL_LOGS_MAX,
expiresAt: 0,
};
}
/** Fields that should always be redacted from logged payloads */
const SENSITIVE_KEYS = new Set([
"api_key",
@@ -212,17 +262,18 @@ export async function saveCallLog(entry: any) {
`
).run(logEntry);
// 2. Trim old entries beyond CALL_LOGS_MAX
// 2. Trim old entries beyond the configured call log cap
const maxCallLogs = await getMaxCallLogs();
const countRow = asRecord(db.prepare("SELECT COUNT(*) as cnt FROM call_logs").get());
const count = toNumber(countRow.cnt);
if (count > CALL_LOGS_MAX) {
if (count > maxCallLogs) {
db.prepare(
`
DELETE FROM call_logs WHERE id IN (
SELECT id FROM call_logs ORDER BY timestamp ASC LIMIT ?
)
`
).run(count - CALL_LOGS_MAX);
).run(count - maxCallLogs);
}
// 3. Write full payload to disk file (untruncated)

View File

@@ -98,6 +98,47 @@ export const CLI_TOOLS = {
{ step: 6, title: "Select Model", type: "modelSelector" },
],
},
windsurf: {
id: "windsurf",
name: "Windsurf",
icon: "airwave",
color: "#06B6D4",
description: "Windsurf IDE guide with current official limitations",
docsUrl: "https://docs.windsurf.com/windsurf/models",
configType: "guide",
notes: [
{
type: "warning",
text: "Official Windsurf docs currently describe BYOK for select Claude models plus enterprise URL/token settings, not a generic custom OpenAI-compatible provider.",
},
{
type: "warning",
text: "The official proxy documentation is for network proxies. Verify the latest Windsurf docs before attempting to point it at OmniRoute as a model provider.",
},
],
guideSteps: [
{
step: 1,
title: "Open AI settings",
desc: "Open Windsurf settings and review the current AI model or subscription configuration.",
},
{
step: 2,
title: "Review BYOK",
desc: "Check the official BYOK/models page for the currently supported provider and model options.",
},
{
step: 3,
title: "Check enterprise setup",
desc: "If your organization uses Windsurf enterprise features, follow the official enterprise URL/token flow from Windsurf docs.",
},
{
step: 4,
title: "Re-verify support",
desc: "Only use OmniRoute directly if Windsurf ships official custom provider or base URL support in a later release.",
},
],
},
cline: {
id: "cline",
name: "Cline",

View File

@@ -40,7 +40,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
supportsThinking: true,
supportsTools: true,
supportsVision: true,
aliases: ["gemini-3-pro-high"],
aliases: ["gemini-3-pro-high", "gemini-3.1-pro-preview", "gemini-3.1-pro-preview-customtools"],
},
// ── Gemini 3.1 Pro Low ──────────────────────────────────────────

View File

@@ -59,6 +59,13 @@ const CLI_TOOLS: Record<string, any> = {
state: ".cursor/agent-cli-state.json",
},
},
windsurf: {
defaultCommand: null,
envBinKey: "CLI_WINDSURF_BIN",
requiresBinary: false,
healthcheckTimeoutMs: 4000,
paths: {},
},
cline: {
defaultCommand: "cline",
envBinKey: "CLI_CLINE_BIN",

View File

@@ -149,6 +149,7 @@ export const updateSettingsSchema = z.object({
instanceName: z.string().max(100).optional(),
corsOrigins: z.string().max(500).optional(),
logRetentionDays: z.number().int().min(1).max(365).optional(),
maxCallLogs: z.number().int().min(1).max(1_000_000).optional(),
cloudUrl: z.string().max(500).optional(),
baseUrl: z.string().max(500).optional(),
setupComplete: z.boolean().optional(),

View File

@@ -18,6 +18,7 @@ export const updateSettingsSchema = z.object({
instanceName: z.string().max(100).optional(),
corsOrigins: z.string().max(500).optional(),
logRetentionDays: z.number().int().min(1).max(365).optional(),
maxCallLogs: z.number().int().min(1).max(1_000_000).optional(),
cloudUrl: z.string().max(500).optional(),
baseUrl: z.string().max(500).optional(),
setupComplete: z.boolean().optional(),

View File

@@ -0,0 +1,54 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-calllogs-cap-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const localDb = await import("../../src/lib/localDb.ts");
const callLogs = await import("../../src/lib/usage/callLogs.ts");
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
callLogs.invalidateCallLogsMaxCache();
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("call logs respect the configurable maxCallLogs setting", async () => {
await localDb.updateSettings({ maxCallLogs: 3 });
callLogs.invalidateCallLogsMaxCache();
for (let i = 1; i <= 5; i++) {
await callLogs.saveCallLog({
method: "POST",
path: "/v1/chat/completions",
status: 200,
model: `model-${i}`,
provider: "openai",
duration: i,
requestBody: { index: i },
responseBody: { ok: true, index: i },
});
}
const logs = await callLogs.getCallLogs({ limit: 10 });
assert.equal(logs.length, 3);
assert.deepEqual(
logs.map((entry) => entry.model),
["model-5", "model-4", "model-3"]
);
});

View File

@@ -37,6 +37,7 @@ describe("CLI_TOOL_IDS", () => {
"droid",
"openclaw",
"cursor",
"windsurf",
"cline",
"kilo",
"continue",
@@ -160,6 +161,15 @@ describe("continue tool — no binary required", () => {
});
});
describe("windsurf tool — guide-only integration", () => {
it("should report installed=true without requiring a local binary", async () => {
const result = await getCliRuntimeStatus("windsurf");
assert.equal(result.installed, true);
assert.equal(result.runnable, true);
assert.equal(result.reason, "not_required");
});
});
// ─── resolveOpencodeConfigPath — cross-platform ─────────────────
const { resolveOpencodeConfigPath: resolveOpencodeConfigPathFn } =

View File

@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
import { getModelInfoCore } from "../../open-sse/services/model.ts";
import { REGISTRY } from "../../open-sse/config/providerRegistry.ts";
import { getStaticModelsForProvider } from "../../src/app/api/providers/[id]/models/route.ts";
test("T28: gemini catalog includes preview models from 9router", () => {
const geminiIds = REGISTRY.gemini.models.map((m) => m.id);
@@ -14,6 +15,20 @@ test("T28: gemini catalog includes preview models from 9router", () => {
assert.ok(geminiCliIds.includes("gemini-3-flash-preview"));
});
test("T28: antigravity static catalog includes Gemini 3.1 preview fallbacks", () => {
const staticIds = (getStaticModelsForProvider("antigravity") || []).map((m) => m.id);
assert.ok(staticIds.includes("gemini-3.1-pro-preview"));
assert.ok(staticIds.includes("gemini-3.1-flash-lite-preview"));
});
test("T28: qwen registry uses DashScope-compatible base URL", () => {
assert.equal(
REGISTRY.qwen.baseUrl,
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions"
);
});
test("T28: vertex catalog includes partner models when vertex executor is available", () => {
const vertexIds = REGISTRY.vertex.models.map((m) => m.id);

View File

@@ -47,7 +47,11 @@ test("T38: modelSpecs exposes centralized helpers with alias and prefix lookup",
assert.equal(typeof MODEL_SPECS["gemini-3.1-pro-high"], "object");
assert.equal(getModelSpec("gemini-3-pro-high").maxOutputTokens, 131072);
assert.equal(getModelSpec("gemini-3-flash-preview").maxOutputTokens, 65536);
assert.equal(getModelSpec("gemini-3.1-pro-preview").maxOutputTokens, 131072);
assert.equal(getModelSpec("gemini-3.1-pro-preview-customtools").maxOutputTokens, 131072);
assert.equal(resolveModelAlias("gemini-3-pro-low"), "gemini-3.1-pro-low");
assert.equal(resolveModelAlias("gemini-3.1-pro-preview"), "gemini-3.1-pro-high");
assert.equal(resolveModelAlias("gemini-3.1-pro-preview-customtools"), "gemini-3.1-pro-high");
assert.equal(getDefaultThinkingBudget("gemini-3.1-pro-high"), 24576);
assert.equal(capThinkingBudget("gemini-3.1-pro-low", 50000), 16000);
});

View File

@@ -65,3 +65,18 @@ test("T40: OpenCode config generator includes endpoint and selected API key", ()
assert.equal(mergedConfig.providers.omniroute.baseURL, "http://localhost:20128/v1");
assert.equal(mergedConfig.providers.omniroute.apiKey, "sk_test_opencode");
});
test("T40: Windsurf card documents current official limitations honestly", () => {
const windsurf = CLI_TOOLS.windsurf;
assert.ok(windsurf, "Windsurf tool card must exist");
assert.equal(windsurf.configType, "guide");
const notesText = (windsurf.notes || [])
.map((note) => note?.text || "")
.join(" ")
.toLowerCase();
assert.match(notesText, /byok/);
assert.match(notesText, /custom openai-compatible provider/);
assert.match(notesText, /proxy documentation is for network proxies/);
});

View File

@@ -0,0 +1,192 @@
import test from "node:test";
import assert from "node:assert/strict";
const {
coerceSchemaNumericFields,
sanitizeToolDescription,
coerceToolSchemas,
sanitizeToolDescriptions,
injectEmptyReasoningContentForToolCalls,
} = await import("../../open-sse/translator/helpers/schemaCoercion.ts");
const { translateRequest } = await import("../../open-sse/translator/index.ts");
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
test("tool sanitization: coerces numeric JSON Schema fields recursively", () => {
const schema = {
type: "object",
properties: {
count: { type: "integer", minimum: "1", maximum: "10" },
items: {
type: "array",
minItems: "2",
items: { type: "string", minLength: "3" },
},
},
};
const result = coerceSchemaNumericFields(schema);
assert.equal(result.properties.count.minimum, 1);
assert.equal(result.properties.count.maximum, 10);
assert.equal(result.properties.items.minItems, 2);
assert.equal(result.properties.items.items.minLength, 3);
});
test("tool sanitization: preserves non-numeric JSON Schema strings", () => {
const schema = {
type: "object",
properties: {
value: { type: "string", minimum: "abc" },
},
};
const result = coerceSchemaNumericFields(schema);
assert.equal(result.properties.value.minimum, "abc");
});
test("tool sanitization: normalizes descriptions across OpenAI, Claude, and Gemini shapes", () => {
const openAITool = sanitizeToolDescription({
type: "function",
function: { name: "sum", description: null, parameters: {} },
});
const claudeTool = sanitizeToolDescription({
name: "sum",
description: 42,
input_schema: { type: "object" },
});
const geminiTool = sanitizeToolDescription({
functionDeclarations: [{ name: "sum", description: false, parameters: {} }],
});
assert.equal(openAITool.function.description, "");
assert.equal(claudeTool.description, "42");
assert.equal(geminiTool.functionDeclarations[0].description, "false");
});
test("tool sanitization: coerces schemas and descriptions in tool arrays", () => {
const tools = sanitizeToolDescriptions(
coerceToolSchemas([
{
type: "function",
function: {
name: "sum",
description: 5,
parameters: {
type: "object",
properties: {
count: { type: "integer", minimum: "1" },
},
},
},
},
])
);
assert.equal(tools[0].function.description, "5");
assert.equal(tools[0].function.parameters.properties.count.minimum, 1);
});
test("translateRequest sanitizes tools before Claude output", () => {
const translated = translateRequest(
FORMATS.OPENAI,
FORMATS.CLAUDE,
"claude-sonnet-4-6",
{
messages: [{ role: "user", content: "hello" }],
tools: [
{
type: "function",
function: {
name: "sum",
description: null,
parameters: {
type: "object",
properties: {
count: { type: "integer", minimum: "1", maximum: "9" },
},
},
},
},
],
},
false,
null,
"claude"
);
assert.equal(translated.tools[0].description, "");
assert.equal(translated.tools[0].input_schema.properties.count.minimum, 1);
assert.equal(translated.tools[0].input_schema.properties.count.maximum, 9);
});
test("translateRequest sanitizes OpenAI tool payloads on passthrough", () => {
const translated = translateRequest(
FORMATS.OPENAI,
FORMATS.OPENAI,
"gpt-5.2",
{
messages: [{ role: "user", content: "hello" }],
tools: [
{
type: "function",
function: {
name: "sum",
description: 7,
parameters: {
type: "object",
properties: {
count: { type: "integer", minimum: "2" },
},
},
},
},
],
},
false,
null,
"openai"
);
assert.equal(translated.tools[0].function.description, "7");
assert.equal(translated.tools[0].function.parameters.properties.count.minimum, 2);
});
test("tool sanitization: injects empty reasoning_content only for DeepSeek tool-call history", () => {
const messages = [
{ role: "user", content: "hello" },
{
role: "assistant",
tool_calls: [{ id: "call_1", type: "function", function: { name: "sum", arguments: "{}" } }],
},
];
const deepseekMessages = injectEmptyReasoningContentForToolCalls(messages, "deepseek");
const openaiMessages = injectEmptyReasoningContentForToolCalls(messages, "openai");
assert.equal(deepseekMessages[1].reasoning_content, "");
assert.equal(openaiMessages[1].reasoning_content, undefined);
});
test("translateRequest injects reasoning_content for DeepSeek assistant tool calls", () => {
const translated = translateRequest(
FORMATS.OPENAI,
FORMATS.OPENAI,
"deepseek-reasoner",
{
messages: [
{ role: "user", content: "hello" },
{
role: "assistant",
tool_calls: [
{ id: "call_1", type: "function", function: { name: "sum", arguments: "{}" } },
],
},
{ role: "tool", tool_call_id: "call_1", content: "3" },
],
},
false,
null,
"deepseek"
);
assert.equal(translated.messages[1].reasoning_content, "");
});