Files
OmniRoute/tests/unit/reasoning-cache.test.ts
Diego Rodrigues de Sa e Souza 9350a5d6c6 Release v3.8.22 (#3623)
* chore(release): open v3.8.22 development cycle

* refactor(dashboard): extract ProviderDetailPageClient — #3501 Phase 0 (#3633)

#3501 Phase 0: extract ProviderDetailPageClient + smoke test.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* refactor(dashboard): extract auth-import modals — #3501 Phase 1a (#3634)

#3501 Phase 1a: extract 3 auth-import modal clusters.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* fix(db): reclassify localDb unexported modules as intentionally-internal (#3499) (#3635)

Closes #3499 — reclassify localDb unexported modules as intentionally-internal (audit + honest gate framing).

* refactor(db): move call_logs aggregations into callLogStats db module (#3500) (#3636)

#3500 slice 1: call_logs aggregations → src/lib/db/callLogStats.ts (Rule #5). Byte-identical queries; TDD 6/6.

* refactor(dashboard): extract EditCompatibleNodeModal — #3501 Phase 1b (#3638)

#3501 Phase 1b: extract EditCompatibleNodeModal (cycle-safe via leaf constants module).

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* refactor(db): move community_servers SQL into gamification db module (#3500 slice 3) (#3639)

#3500 slice 3: community_servers SQL → gamification db module.

* refactor(db): move usage_history SQL into usageAnalytics module (#3500 slice 2) (#3644)

#3500 slice 2: usage_history/daily_usage_summary SQL → usageAnalytics db module.

* refactor(db): move skills UPDATE + db-backups SQL into db modules (#3500 slice 5) (#3647)

#3500 slice 5: skills UPDATE (allowlist) + db-backups SQL → db modules.

* refactor(db): move usage_logs/semantic_cache/proxy_logs SQL into db modules (#3500 slice 4) (#3648)

#3500 slice 4: usage_logs/semantic_cache/proxy_logs SQL → db modules. All internal routes done (2 external by-design remain).

* chore(db-gate): reclassify external-DB reads, fully close #3500 (#3649)

Closes #3500: reclassify external-DB reads; all internal raw-SQL migrated to db/ modules.

* refactor(dashboard): extract pure helpers to providerPageHelpers — #3501 Phase 2 (#3653)

#3501 Phase 2: extract pure helpers to providerPageHelpers (leaf, cycle-safe).

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* refactor(dashboard): extract remaining shared helpers to providerPageHelpers — #3501 Phase 2b (#3658)

#3501 Phase 2b: extract remaining shared helpers to providerPageHelpers (leaf, cycle-safe). Heavy modals unblocked.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* fix(reasoning): replay reasoning_content on plain DeepSeek turns (#1682) (#3632)

Integrated into release/v3.8.22

* fix(kiro): route enterprise IAM Identity Center accounts to their regional endpoint (#3631)

Integrated into release/v3.8.22

* refactor: small code cleanup (#3523)

Integrated into release/v3.8.22

* fix(combo): skip same-provider targets on 408/500/502/503/504/524 errors (#3637)

Integrated into release/v3.8.22 — circuit-breaker guard added in review (#1731v2)

* feat(providers): add MiMoCode free-tier provider with bootstrap JWT auth (#3659)

Integrated into release/v3.8.22 — page.tsx conflict resolved + NoAuthAccountCard re-applied to ProviderDetailPageClient in review. MiMoCode endpoint validated live.

* Log Responses WebSocket calls in history (#3616)

Integrated into release/v3.8.22 — Codex Responses WebSocket call history logging.

* Add Claude Code routing preference for unprefixed Claude models (#3540)

Integrated into release/v3.8.22 — page.tsx conflict resolved (re-applied toggle to ProviderDetailPageClient) + disable-test updated for catalog drift in review.

* docs(changelog): credit #3632/#3631/#3637/#3659/#3540/#3616/#3523 (v3.8.22 targeted review round)

* fix(mimocode): add required authHeader:"none" to registry entry (#3659 follow-up)

The mimocode RegistryEntry omitted the required authHeader field, which broke
typecheck:core (TS2741). Match the no-auth convention (authType:"none" + authHeader:"none")
used by veoaifree-web and other free providers. Follow-up to #3659 (@pizzav-xyz).

* fix(responses): detect stream readiness for tool-call-only and object-less chunks (#3612) (#3661)

Closes #3612

* fix(mitm): remove duplicated 'Command failed:' error prefix (#3641) (#3662)

Closes #3641

* fix(cli): honor HERMES_HOME for Hermes Agent config path (#3628) (#3663)

Closes #3628

* fix(api): fetch live OpenCode model catalog for no-auth model picker (#3611) (#3664)

Closes #3611

* fix(api): flag provider topology error state by current status, not stale history (#3619) (#3666)

Closes #3619

* fix(electron): launch peer-stamping server-ws.mjs entrypoint to avoid 403 LOCAL_ONLY (#3386) (#3665)

Closes #3386

* fix(dashboard): restore home topology live in-flight pulse (#3507) (#3667)

Closes #3507

* fix(oauth): name Kiro/AWS auto-imported accounts and dedupe by profileArn (#3615) (#3671)

Closes #3615

* fix(resilience): clear stale transient connection cooldowns on startup (#3625) (#3672)

Closes #3625

* fix(i18n): use logical CSS direction utilities for sidebar and key overlays (RTL #3541) (#3670)

Closes #3541

* fix(dashboard): honor auto-hide and switch to visible filter on passthrough Test-all (#3610) (#3669)

Closes #3610

* refactor(dashboard): extract AddApiKeyModal + EditConnectionModal — #3501 Phase 1c (#3674)

#3501 Phase 1c: extract AddApiKeyModal, EditConnectionModal, WebSessionCredentialGuide into components/; god-component 10,166->8,092 LOC. Reconciles the v3.8.22 file-size drift for this file.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* docs(changelog): reconcile v3.8.22 — credit #3621/#3622 + MiMoCode follow-up roll-up

* refactor(dashboard): extract ConnectionRow + ModelCompatPopover + SiliconFlowEndpointModal — #3501 Phase 1d (#3676)

#3501 Phase 1d: god-component 8,092->6,838 LOC.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* feat(obsidian): add WebDAV config route + encrypt creds at rest (#3485 part 1) (#3677)

Part 1 of #3485. Adds /api/settings/obsidian/webdav (GET/POST/DELETE) wiring the ready obsidianSync lib, encrypts webdav password + obsidian token at rest, removes the duplicate UI block, drops the KNOWN_MISSING entry. WebDAV file server is part 2.

* feat(obsidian): add /api/v1/webdav file server for Obsidian vault sync (#3485 part 2) (#3678)

Part 2 of #3485. WebDAV server (PROPFIND/GET/PUT/DELETE/MKCOL/MOVE/OPTIONS) handled in the custom server layer (standalone-server-ws.mjs) since the App Router cannot export WebDAV methods. Basic-Auth (constant-time), path-traversal hardened, password decrypt ported from encryption.ts (parity-tested), DATA_DIR resolution parity-tested against dataPaths.ts. End-to-end Obsidian-over-Tailscale validation is a live VPS step (Rule #18).

* fix(combo): stop premature context compaction — real auto-combo windows + per-target compression limit (#3680)

Integrated into release/v3.8.22

* feat(dashboard): deactivate/activate accounts from the quota overview (#3675)

Integrated into release/v3.8.22

* fix(dashboard): close review gaps in bulk provider connection actions (#3271 follow-up) (#3673)

Integrated into release/v3.8.22 — page.tsx conflict (god-component split #3501) resolved by re-applying the bulk-action deltas to ProviderDetailPageClient.tsx

* refactor(dashboard): extract useModelCompatState hook + model sections — #3501 Phase 1e (#3683)

#3501 Phase 1e: extract useModelCompatState hook (unblocks the model sections) + ModelRow/PassthroughModelsSection/PassthroughModelRow/CustomModelsSection/CompatibleModelsSection. god-component 6,838->4,921 LOC.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* refactor(dashboard): extract useProviderConnections/Settings/Models hooks — #3501 Phase 1f (#3684)

#3501 Phase 1f: god-component 4,948->4,062 LOC. Connection state+handlers, settings, and model metadata moved into hooks/.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* chore(release): v3.8.22 CHANGELOG + env-doc sync

- Set release date in CHANGELOG [3.8.22] to 2026-06-11
- Add HERMES_HOME to .env.example (from #3628/#3663)
- Add HERMES_HOME + OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS to ENVIRONMENT.md (#3628/#3540)

* docs(changelog): credit #3673 + #3675 — leninejunior bulk-actions + quota-toggle

---------

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: Abhishek Divekar <adivekar@utexas.edu>
Co-authored-by: NOXX - Commiter <artur1992123@mail.ru>
Co-authored-by: Nicolas Lorin <androw95220@gmail.com>
Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
Co-authored-by: PizzaV <103120356+pizzav-xyz@users.noreply.github.com>
Co-authored-by: kkkayye <98376609+kkkayye@users.noreply.github.com>
Co-authored-by: Witroch4 <witalo_rocha@hotmail.com>
Co-authored-by: Lenine Júnior <lenine@engrene.com.br>
2026-06-11 18:52:29 -03:00

980 lines
31 KiB
TypeScript

/**
* Unit tests for the Reasoning Replay Cache (Issue #1628).
*
* Covers: memory cache, DB fallback, hit/miss counters,
* provider detection, and cleanup behavior.
*/
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-reasoning-"));
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "reasoning-cache-test-secret";
// ──────────── Direct service import ────────────
import {
cacheReasoningFromAssistantMessage,
cacheReasoning,
cacheReasoningByKey,
cacheReasoningBatch,
deleteReasoningCacheEntry,
getReasoningCacheServiceEntries,
lookupReasoning,
recordReplay,
getReasoningCacheServiceStats,
clearReasoningCacheAll,
isDeepSeekReasoningModel,
requiresReasoningReplay,
cleanupReasoningCache,
} from "../../open-sse/services/reasoningCache.ts";
import { translateRequest } from "../../open-sse/translator/index.ts";
import { FORMATS } from "../../open-sse/translator/formats.ts";
import { ensureToolCallIds } from "../../open-sse/translator/helpers/toolCallHelper.ts";
import { getDbInstance } from "../../src/lib/db/core.ts";
import { getReasoningCache, setReasoningCache } from "../../src/lib/db/reasoningCache.ts";
import { DELETE, GET } from "../../src/app/api/cache/reasoning/route.ts";
import { createApiKey } from "../../src/lib/db/apiKeys.ts";
import { updateSettings } from "../../src/lib/db/settings";
import {
clearModelsDevCapabilities,
saveModelsDevCapabilities,
} from "../../src/lib/modelsDevSync.ts";
function buildCapability(overrides = {}) {
return {
tool_call: null,
reasoning: null,
attachment: null,
structured_output: null,
temperature: null,
modalities_input: "[]",
modalities_output: "[]",
knowledge_cutoff: null,
release_date: null,
last_updated: null,
status: null,
family: null,
open_weights: null,
limit_context: null,
limit_input: null,
limit_output: null,
interleaved_field: null,
...overrides,
};
}
before(async () => {
await updateSettings({ requireLogin: false });
});
after(async () => {
await updateSettings({ requireLogin: true });
});
describe("Reasoning Replay Cache — Service Layer", () => {
before(() => {
// Start each suite with a clean slate
clearReasoningCacheAll();
});
after(() => {
clearReasoningCacheAll();
});
it("should store and retrieve reasoning by tool_call_id", () => {
cacheReasoning(
"call_test_1",
"deepseek",
"deepseek-reasoner",
"The user wants to read the file..."
);
const result = lookupReasoning("call_test_1");
assert.equal(result, "The user wants to read the file...");
assert.equal(getReasoningCache("call_test_1")?.reasoning, "The user wants to read the file...");
});
it("should fall back to SQLite when memory misses", () => {
clearReasoningCacheAll();
setReasoningCache("call_db_only", "deepseek", "deepseek-reasoner", "DB-only reasoning");
assert.equal(lookupReasoning("call_db_only"), "DB-only reasoning");
const stats = getReasoningCacheServiceStats();
assert.equal(stats.hits, 1);
assert.equal(stats.memoryEntries, 1);
assert.equal(stats.dbEntries, 1);
});
it("should return null for unknown tool_call_id", () => {
const result = lookupReasoning("call_nonexistent");
assert.equal(result, null);
});
it("should return null for empty tool_call_id", () => {
const result = lookupReasoning("");
assert.equal(result, null);
});
it("should skip caching when reasoning is empty", () => {
cacheReasoning("call_empty", "deepseek", "deepseek-chat", "");
const result = lookupReasoning("call_empty");
assert.equal(result, null);
});
it("should cache reasoning for multiple tool_call_ids (batch)", () => {
cacheReasoningBatch(
["call_batch_1", "call_batch_2", "call_batch_3"],
"deepseek",
"deepseek-reasoner",
"Batch reasoning content"
);
assert.equal(lookupReasoning("call_batch_1"), "Batch reasoning content");
assert.equal(lookupReasoning("call_batch_2"), "Batch reasoning content");
assert.equal(lookupReasoning("call_batch_3"), "Batch reasoning content");
});
it("should capture assistant reasoning for all tool_call IDs", () => {
clearReasoningCacheAll();
const cached = cacheReasoningFromAssistantMessage(
{
role: "assistant",
reasoning_content: "Captured assistant reasoning",
tool_calls: [{ id: "call_capture_1" }, { id: "call_capture_2" }],
},
"deepseek",
"deepseek-reasoner"
);
assert.equal(cached, 2);
assert.equal(lookupReasoning("call_capture_1"), "Captured assistant reasoning");
assert.equal(lookupReasoning("call_capture_2"), "Captured assistant reasoning");
});
it("should keep request message cache keys stable when tool call IDs change", () => {
clearReasoningCacheAll();
const requestId = "req_reasoning_stable";
const messageIndex = 2;
const cacheKey = `${requestId}:${messageIndex}`;
const body = {
messages: [
{
role: "assistant",
tool_calls: [
{
id: "call_before_normalization",
type: "function",
function: { name: "lookup", arguments: { city: "Seoul" } },
},
],
},
{ role: "tool", content: "Sunny" },
],
};
cacheReasoning(cacheKey, "deepseek", "deepseek-reasoner", "Stable cached reasoning");
const originalToolCallId = body.messages[0].tool_calls[0].id;
ensureToolCallIds(body, { use9CharId: true });
assert.notEqual(body.messages[0].tool_calls[0].id, originalToolCallId);
assert.equal(lookupReasoning(cacheKey), "Stable cached reasoning");
});
it("should capture provider reasoning alias when reasoning_content is absent", () => {
clearReasoningCacheAll();
const cached = cacheReasoningFromAssistantMessage(
{
role: "assistant",
reasoning: "Alias reasoning",
tool_calls: [{ id: "call_capture_alias" }],
},
"kimi",
"kimi-k2.5"
);
assert.equal(cached, 1);
assert.equal(lookupReasoning("call_capture_alias"), "Alias reasoning");
});
it("should cache assistant reasoning without tool calls by request and message index", () => {
clearReasoningCacheAll();
const cached = cacheReasoningFromAssistantMessage(
{
role: "assistant",
reasoning_content: "No tool call reasoning",
},
"deepseek",
"deepseek-reasoner",
{ requestId: "req_no_tools", messageIndex: 3 }
);
assert.equal(cached, 1);
assert.equal(lookupReasoning("request:req_no_tools:message:3"), "No tool call reasoning");
});
it("should skip assistant reasoning without tool calls when stable key context is absent", () => {
clearReasoningCacheAll();
const cached = cacheReasoningFromAssistantMessage(
{
role: "assistant",
reasoning_content: "Missing key context",
},
"deepseek",
"deepseek-reasoner"
);
assert.equal(cached, 0);
assert.equal(lookupReasoning("request:req_missing:message:0"), null);
});
it("should store arbitrary reasoning cache keys", () => {
clearReasoningCacheAll();
cacheReasoningByKey(
"request:req_direct:message:1",
"deepseek",
"deepseek-reasoner",
"Keyed plan"
);
assert.equal(lookupReasoning("request:req_direct:message:1"), "Keyed plan");
assert.equal(getReasoningCache("request:req_direct:message:1")?.reasoning, "Keyed plan");
});
it("should not overwrite if same tool_call_id is cached again", () => {
cacheReasoning("call_overwrite", "deepseek", "deepseek-chat", "First reasoning");
cacheReasoning("call_overwrite", "deepseek", "deepseek-chat", "Updated reasoning");
// Second write wins (INSERT OR REPLACE)
const result = lookupReasoning("call_overwrite");
assert.equal(result, "Updated reasoning");
});
it("should track hits and misses correctly", () => {
clearReasoningCacheAll();
cacheReasoning("call_hit_test", "deepseek", "deepseek-chat", "test reasoning");
lookupReasoning("call_hit_test"); // hit
lookupReasoning("call_hit_test"); // hit
lookupReasoning("call_miss_test"); // miss
const stats = getReasoningCacheServiceStats();
assert.ok(stats.hits >= 2, `Expected at least 2 hits, got ${stats.hits}`);
assert.ok(stats.misses >= 1, `Expected at least 1 miss, got ${stats.misses}`);
});
it("should track replays", () => {
clearReasoningCacheAll();
recordReplay();
recordReplay();
recordReplay();
const stats = getReasoningCacheServiceStats();
assert.ok(stats.replays >= 3, `Expected at least 3 replays, got ${stats.replays}`);
});
it("should report correct stats structure", () => {
clearReasoningCacheAll();
cacheReasoning("call_stat_1", "deepseek", "deepseek-reasoner", "Reasoning A");
cacheReasoning("call_stat_2", "kimi", "kimi-k2.5", "Reasoning B from Kimi");
const stats = getReasoningCacheServiceStats();
assert.equal(typeof stats.memoryEntries, "number");
assert.equal(typeof stats.dbEntries, "number");
assert.equal(typeof stats.totalEntries, "number");
assert.equal(typeof stats.totalChars, "number");
assert.equal(typeof stats.hits, "number");
assert.equal(typeof stats.misses, "number");
assert.equal(typeof stats.replays, "number");
assert.equal(typeof stats.replayRate, "string");
assert.ok(stats.replayRate.endsWith("%"));
assert.equal(typeof stats.byProvider, "object");
assert.equal(typeof stats.byModel, "object");
assert.equal(stats.dbEntries, 2);
assert.equal(stats.byProvider.deepseek.entries, 1);
assert.equal(stats.byProvider.kimi.entries, 1);
});
it("should list persisted entries for the dashboard API", () => {
clearReasoningCacheAll();
cacheReasoning("call_entry_1", "deepseek", "deepseek-reasoner", "Entry reasoning A");
cacheReasoning("call_entry_2", "kimi", "kimi-k2.5", "Entry reasoning B");
const deepseekEntries = getReasoningCacheServiceEntries({ provider: "deepseek" }) as Array<{
toolCallId: string;
expiresAt: string;
}>;
assert.equal(deepseekEntries.length, 1);
assert.equal(deepseekEntries[0].toolCallId, "call_entry_1");
assert.doesNotThrow(() => new Date(deepseekEntries[0].expiresAt).toISOString());
});
it("should clear all entries", () => {
cacheReasoning("call_clear_1", "deepseek", "deepseek-chat", "Will be cleared");
cacheReasoning("call_clear_2", "deepseek", "deepseek-chat", "Also cleared");
const count = clearReasoningCacheAll();
assert.ok(count >= 0);
assert.equal(lookupReasoning("call_clear_1"), null);
assert.equal(lookupReasoning("call_clear_2"), null);
});
it("should delete one entry by tool_call_id", () => {
clearReasoningCacheAll();
cacheReasoning("call_delete_1", "deepseek", "deepseek-chat", "Delete me");
cacheReasoning("call_delete_2", "deepseek", "deepseek-chat", "Keep me");
assert.equal(deleteReasoningCacheEntry("call_delete_1"), 1);
assert.equal(lookupReasoning("call_delete_1"), null);
assert.equal(lookupReasoning("call_delete_2"), "Keep me");
});
it("should clear entries by provider only", () => {
clearReasoningCacheAll();
cacheReasoning("call_provider_ds", "deepseek", "deepseek-chat", "DeepSeek reasoning");
cacheReasoning("call_provider_kimi", "kimi", "kimi-k2.5", "Kimi reasoning");
assert.equal(clearReasoningCacheAll("deepseek"), 1);
assert.equal(lookupReasoning("call_provider_ds"), null);
assert.equal(lookupReasoning("call_provider_kimi"), "Kimi reasoning");
});
it("should cleanup expired reasoning (no-op when nothing expired)", () => {
cacheReasoning("call_cleanup_test", "deepseek", "deepseek-chat", "Not expired yet");
const cleaned = cleanupReasoningCache();
assert.equal(typeof cleaned, "number");
// Entry should still be available since TTL is 2 hours
assert.equal(lookupReasoning("call_cleanup_test"), "Not expired yet");
});
it("should not return expired SQLite entries and cleanup should prune them", () => {
clearReasoningCacheAll();
setReasoningCache("call_expired", "deepseek", "deepseek-chat", "Expired reasoning", -1_000);
assert.equal(lookupReasoning("call_expired"), null);
assert.equal(cleanupReasoningCache(), 1);
assert.equal(getReasoningCacheServiceStats().dbEntries, 0);
});
it("should read and prune legacy ISO expires_at rows", () => {
clearReasoningCacheAll();
const db = getDbInstance();
const futureIso = new Date(Date.now() + 60_000).toISOString();
const expiredIso = new Date(Date.now() - 60_000).toISOString();
db.prepare(
`INSERT INTO reasoning_cache
(tool_call_id, provider, model, reasoning, char_count, created_at, expires_at)
VALUES (?, ?, ?, ?, ?, datetime('now'), ?)`
).run(
"call_legacy_iso_active",
"deepseek",
"deepseek-chat",
"Legacy ISO reasoning",
"Legacy ISO reasoning".length,
futureIso
);
db.prepare(
`INSERT INTO reasoning_cache
(tool_call_id, provider, model, reasoning, char_count, created_at, expires_at)
VALUES (?, ?, ?, ?, ?, datetime('now'), ?)`
).run(
"call_legacy_iso_expired",
"deepseek",
"deepseek-chat",
"Expired legacy ISO reasoning",
"Expired legacy ISO reasoning".length,
expiredIso
);
assert.equal(lookupReasoning("call_legacy_iso_active"), "Legacy ISO reasoning");
assert.equal(lookupReasoning("call_legacy_iso_expired"), null);
const entries = getReasoningCacheServiceEntries({ provider: "deepseek" }) as Array<{
toolCallId: string;
expiresAt: string;
}>;
assert.equal(
entries.some((entry) => entry.expiresAt === futureIso),
true
);
assert.equal(cleanupReasoningCache(), 1);
});
});
describe("Reasoning Replay Cache — Provider Detection", () => {
it("should detect deepseek as requiring replay", () => {
assert.equal(requiresReasoningReplay({ provider: "deepseek", model: "deepseek-chat" }), true);
});
it("should detect opencode-go as requiring replay", () => {
assert.equal(requiresReasoningReplay({ provider: "opencode-go", model: "some-model" }), true);
});
it("should not replay legacy deepseek-r1 even under replay providers", () => {
assert.equal(requiresReasoningReplay({ provider: "siliconflow", model: "deepseek-r1" }), false);
});
it("should not replay deepseek-r1 model pattern", () => {
assert.equal(
requiresReasoningReplay({ provider: "unknown-provider", model: "deepseek-r1" }),
false
);
});
it("should detect deepseek-reasoner model pattern", () => {
assert.equal(
requiresReasoningReplay({ provider: "unknown-provider", model: "deepseek-reasoner" }),
false
);
});
it("should detect DeepSeek V4 model pattern", () => {
assert.equal(
requiresReasoningReplay({ provider: "unknown-provider", model: "deepseek/v4-pro" }),
true
);
});
it("should detect DeepSeek V4 thinking mode explicitly", () => {
assert.equal(
isDeepSeekReasoningModel({
provider: "unknown-provider",
model: "deepseek-v4.flash",
thinkingEnabled: true,
}),
true
);
});
it("should NOT detect DeepSeek V4 when thinking mode is disabled", () => {
assert.equal(
isDeepSeekReasoningModel({
provider: "unknown-provider",
model: "deepseek-v4.flash",
thinkingEnabled: false,
}),
false
);
});
it("should detect kimi-k2 model pattern", () => {
assert.equal(
requiresReasoningReplay({ provider: "unknown-provider", model: "kimi-k2.5" }),
true
);
});
it("should detect qwq model pattern", () => {
assert.equal(
requiresReasoningReplay({ provider: "unknown-provider", model: "qwq-32b-preview" }),
true
);
});
it("should detect qwen-thinking model pattern", () => {
assert.equal(
requiresReasoningReplay({ provider: "unknown-provider", model: "qwen3-thinking-235b" }),
true
);
});
it("should detect GLM thinking model pattern", () => {
assert.equal(requiresReasoningReplay({ provider: "glm", model: "glm-5-thinking" }), true);
});
it("should detect xiaomi-mimo provider", () => {
// MiMo enforces reasoning_content echo on subsequent turns; without
// replay the upstream returns 400 "Param Incorrect: The reasoning_content
// in the thinking mode must be passed back to the API."
assert.equal(
requiresReasoningReplay({ provider: "xiaomi-mimo", model: "mimo-v2.5-pro" }),
true
);
assert.equal(requiresReasoningReplay({ provider: "XIAOMI-MIMO", model: "mimo-v2.5" }), true);
});
it("should detect mimo-v* model pattern under any provider id", () => {
assert.equal(
requiresReasoningReplay({ provider: "unknown-provider", model: "mimo-v2.5-pro" }),
true
);
assert.equal(requiresReasoningReplay({ provider: "unknown-provider", model: "mimo-v3" }), true);
assert.equal(
requiresReasoningReplay({ provider: "unknown-provider", model: "MimoV2.5-pro" }),
true
);
});
it("should NOT detect a generic openai model", () => {
assert.equal(requiresReasoningReplay({ provider: "openai", model: "gpt-4o" }), false);
});
it("should NOT detect claude as requiring replay", () => {
assert.equal(requiresReasoningReplay({ provider: "anthropic", model: "claude-opus-4" }), false);
});
});
describe("Reasoning Replay Cache — Translator Replay", () => {
before(() => {
clearReasoningCacheAll();
clearModelsDevCapabilities();
});
after(() => {
clearReasoningCacheAll();
clearModelsDevCapabilities();
});
function translateWithToolHistory(provider: string, model: string, callId: string) {
return translateRequest(
FORMATS.OPENAI,
FORMATS.OPENAI,
model,
{
messages: [
{ role: "user", content: "use a tool" },
{
role: "assistant",
content: null,
tool_calls: [
{ id: callId, type: "function", function: { name: "read_file", arguments: "{}" } },
],
},
{ role: "tool", tool_call_id: callId, content: "tool result" },
],
},
false,
null,
provider
);
}
it("should inject cached reasoning for DeepSeek instead of empty fallback", () => {
clearReasoningCacheAll();
clearModelsDevCapabilities();
saveModelsDevCapabilities({
deepseek: {
"deepseek-reasoner": buildCapability({
interleaved_field: "reasoning_content",
reasoning: true,
tool_call: true,
}),
},
});
cacheReasoning("call_translate_ds", "deepseek", "deepseek-reasoner", "DeepSeek cached plan");
const translated = translateWithToolHistory(
"deepseek",
"deepseek-reasoner",
"call_translate_ds"
);
assert.equal(translated.messages[1].reasoning_content, "DeepSeek cached plan");
assert.equal(getReasoningCacheServiceStats().replays, 1);
});
it("should preserve client-provided reasoning content", () => {
clearReasoningCacheAll();
clearModelsDevCapabilities();
saveModelsDevCapabilities({
deepseek: {
"deepseek-reasoner": buildCapability({
interleaved_field: "reasoning_content",
reasoning: true,
tool_call: true,
}),
},
});
cacheReasoning("call_preserve", "deepseek", "deepseek-reasoner", "Cached reasoning");
const translated = translateRequest(
FORMATS.OPENAI,
FORMATS.OPENAI,
"deepseek-reasoner",
{
messages: [
{ role: "user", content: "use a tool" },
{
role: "assistant",
content: null,
reasoning_content: "Client reasoning",
tool_calls: [
{
id: "call_preserve",
type: "function",
function: { name: "tool", arguments: "{}" },
},
],
},
],
},
false,
null,
"deepseek"
);
assert.equal(translated.messages[1].reasoning_content, "Client reasoning");
assert.equal(getReasoningCacheServiceStats().replays, 0);
});
it("should inject cached reasoning for Qwen and GLM thinking models", () => {
clearReasoningCacheAll();
clearModelsDevCapabilities();
saveModelsDevCapabilities({
qwen: {
"qwen3-thinking-235b": buildCapability({
interleaved_field: "reasoning_content",
reasoning: true,
tool_call: true,
}),
},
glm: {
"glm-5-thinking": buildCapability({
interleaved_field: "reasoning_content",
reasoning: true,
tool_call: true,
}),
},
});
cacheReasoning("call_qwen_think", "qwen", "qwen3-thinking-235b", "Qwen cached plan");
cacheReasoning("call_glm_think", "glm", "glm-5-thinking", "GLM cached plan");
const qwen = translateWithToolHistory("qwen", "qwen3-thinking-235b", "call_qwen_think");
const glm = translateWithToolHistory("glm", "glm-5-thinking", "call_glm_think");
assert.equal(qwen.messages[1].reasoning_content, "Qwen cached plan");
assert.equal(glm.messages[1].reasoning_content, "GLM cached plan");
assert.equal(getReasoningCacheServiceStats().replays, 2);
});
it("should not inject reasoning_content for generic non-reasoning providers", () => {
clearReasoningCacheAll();
clearModelsDevCapabilities();
cacheReasoning("call_openai", "openai", "gpt-4o", "Should not replay");
const translated = translateWithToolHistory("openai", "gpt-4o", "call_openai");
assert.equal(translated.messages[1].reasoning_content, undefined);
assert.equal(getReasoningCacheServiceStats().replays, 0);
});
it("should support the full capture then replay flow", () => {
clearReasoningCacheAll();
clearModelsDevCapabilities();
saveModelsDevCapabilities({
deepseek: {
"deepseek-reasoner": buildCapability({
interleaved_field: "reasoning_content",
reasoning: true,
tool_call: true,
}),
},
});
const captured = cacheReasoningFromAssistantMessage(
{
role: "assistant",
reasoning_content: "Full flow cached plan",
tool_calls: [{ id: "call_full_flow", type: "function" }],
},
"deepseek",
"deepseek-reasoner"
);
const translated = translateWithToolHistory("deepseek", "deepseek-reasoner", "call_full_flow");
assert.equal(captured, 1);
assert.equal(translated.messages[1].reasoning_content, "Full flow cached plan");
assert.equal(getReasoningCacheServiceStats().replays, 1);
});
it("should strip reasoning_content when model has no interleaved replay signal", () => {
clearReasoningCacheAll();
clearModelsDevCapabilities();
const translated = translateRequest(
FORMATS.OPENAI,
FORMATS.OPENAI,
"deepseek-reasoner",
{
messages: [
{ role: "user", content: "hello" },
{
role: "assistant",
content: "ok",
reasoning_content: "should be stripped",
},
],
},
false,
null,
"deepseek"
);
assert.equal(translated.messages[1].reasoning_content, undefined);
});
it("should not inject reasoning_content when interleaved field is reasoning_details", () => {
clearReasoningCacheAll();
clearModelsDevCapabilities();
saveModelsDevCapabilities({
testprovider: {
"test-reasoning-details": buildCapability({
interleaved_field: "reasoning_details",
reasoning: true,
tool_call: true,
}),
},
});
cacheReasoning("call_details", "testprovider", "test-reasoning-details", "cached");
const translated = translateWithToolHistory(
"testprovider",
"test-reasoning-details",
"call_details"
);
assert.equal(translated.messages[1].reasoning_content, undefined);
});
it("should replace empty-string reasoning_content with NON_ANTHROPIC_THINKING_PLACEHOLDER on cache miss", async () => {
// Regression: injectEmptyReasoningContentForToolCalls (schemaCoercion.ts) pre-sets
// reasoning_content="" before the cache lookup. The old condition
// `msg.reasoning_content === undefined` never fired on cache miss, leaving the
// empty string in place. DeepSeek V4+ rejects "" with a 400.
clearReasoningCacheAll();
clearModelsDevCapabilities();
saveModelsDevCapabilities({
deepseek: {
"deepseek-v4-flash": buildCapability({
interleaved_field: "reasoning_content",
reasoning: true,
tool_call: true,
}),
},
});
const { NON_ANTHROPIC_THINKING_PLACEHOLDER } =
await import("../../open-sse/translator/helpers/claudeHelper.ts");
// No cache entry → cache miss
const translated = translateRequest(
FORMATS.OPENAI,
FORMATS.OPENAI,
"deepseek-v4-flash",
{
messages: [
{ role: "user", content: "use a tool" },
{
role: "assistant",
content: null,
reasoning_content: "",
tool_calls: [
{
id: "call_empty_rc",
type: "function",
function: { name: "read_file", arguments: "{}" },
},
],
},
{ role: "tool", tool_call_id: "call_empty_rc", content: "file contents" },
],
},
false,
null,
"deepseek"
);
assert.equal(
translated.messages[1].reasoning_content,
NON_ANTHROPIC_THINKING_PLACEHOLDER,
"empty reasoning_content should be replaced with placeholder on cache miss"
);
});
it("should inject placeholder for a plain (non-tool-call) DeepSeek turn missing reasoning_content (#1682)", async () => {
// Regression (#1682): a multi-turn text conversation where the prior assistant
// turn has NO tool calls and the client (e.g. Cursor) stripped reasoning_content
// from history. DeepSeek V4+ still requires reasoning_content on every assistant
// message in thinking mode, so without a placeholder the upstream returns 400.
clearReasoningCacheAll();
clearModelsDevCapabilities();
saveModelsDevCapabilities({
deepseek: {
"deepseek-v4-pro": buildCapability({
interleaved_field: "reasoning_content",
reasoning: true,
tool_call: true,
}),
},
});
const { NON_ANTHROPIC_THINKING_PLACEHOLDER } =
await import("../../open-sse/translator/helpers/claudeHelper.ts");
const translated = translateRequest(
FORMATS.OPENAI,
FORMATS.OPENAI,
"deepseek-v4-pro",
{
messages: [
{ role: "user", content: "hi" },
// Plain assistant turn, no tool_calls, reasoning_content stripped by client.
{ role: "assistant", content: "Hello! How can I help?" },
{ role: "user", content: "tell me more" },
],
},
false,
null,
"deepseek"
);
assert.equal(
translated.messages[1].reasoning_content,
NON_ANTHROPIC_THINKING_PLACEHOLDER,
"plain DeepSeek assistant turn missing reasoning_content should get the placeholder"
);
});
it("should replay cached reasoning for a plain (non-tool-call) DeepSeek turn when available (#1682)", () => {
// When a request_id-keyed cache entry exists for the plain turn, the real
// reasoning is replayed instead of the placeholder.
clearReasoningCacheAll();
clearModelsDevCapabilities();
saveModelsDevCapabilities({
deepseek: {
"deepseek-v4-pro": buildCapability({
interleaved_field: "reasoning_content",
reasoning: true,
tool_call: true,
}),
},
});
// NOTE: the non-tool-call cache key is built as `getAssistantMessageCacheKey(result, 0)`
// — the message index is hardcoded to 0 in the translator, so the key is always
// `request:<id>:message:0` regardless of the assistant message's actual position.
cacheReasoning(
"request:req-plain-1:message:0",
"deepseek",
"deepseek-v4-pro",
"Real cached plain-turn reasoning"
);
const translated = translateRequest(
FORMATS.OPENAI,
FORMATS.OPENAI,
"deepseek-v4-pro",
{
request_id: "req-plain-1",
messages: [
{ role: "user", content: "hi" },
{ role: "assistant", content: "Hello! How can I help?" },
{ role: "user", content: "tell me more" },
],
},
false,
null,
"deepseek"
);
assert.equal(
translated.messages[1].reasoning_content,
"Real cached plain-turn reasoning",
"plain DeepSeek assistant turn should replay the real cached reasoning when present"
);
assert.equal(getReasoningCacheServiceStats().replays, 1);
});
});
describe("Reasoning Replay Cache — API Route", () => {
let managementApiKey: string;
before(() => {
clearReasoningCacheAll();
});
before(async () => {
const created = await createApiKey("reasoning-cache-route-test", "machine-reasoning", [
"manage",
]);
managementApiKey = created.key;
});
after(() => {
clearReasoningCacheAll();
});
function authedRequest(url: string): Request {
return new Request(url, {
headers: { authorization: `Bearer ${managementApiKey}` },
});
}
it("should return stats and entries from GET", async () => {
clearReasoningCacheAll();
cacheReasoning("call_api_get", "deepseek", "deepseek-reasoner", "API visible reasoning");
const response = await GET(
authedRequest("http://localhost/api/cache/reasoning?provider=deepseek") as never
);
const body = await response.json();
assert.equal(response.status, 200);
assert.equal(body.stats.dbEntries, 1);
assert.equal(body.entries.length, 1);
assert.equal(body.entries[0].toolCallId, "call_api_get");
});
it("should delete a single entry by toolCallId", async () => {
clearReasoningCacheAll();
cacheReasoning("call_api_delete_1", "deepseek", "deepseek-reasoner", "Delete API");
cacheReasoning("call_api_delete_2", "deepseek", "deepseek-reasoner", "Keep API");
const response = await DELETE(
authedRequest("http://localhost/api/cache/reasoning?toolCallId=call_api_delete_1") as never
);
const body = await response.json();
assert.equal(response.status, 200);
assert.equal(body.scope, "toolCallId");
assert.equal(body.cleared, 1);
assert.equal(lookupReasoning("call_api_delete_1"), null);
assert.equal(lookupReasoning("call_api_delete_2"), "Keep API");
});
it("should delete entries by provider", async () => {
clearReasoningCacheAll();
cacheReasoning("call_api_provider_ds", "deepseek", "deepseek-reasoner", "Delete provider");
cacheReasoning("call_api_provider_kimi", "kimi", "kimi-k2.5", "Keep provider");
const response = await DELETE(
authedRequest("http://localhost/api/cache/reasoning?provider=deepseek") as never
);
const body = await response.json();
assert.equal(response.status, 200);
assert.equal(body.scope, "provider");
assert.equal(body.cleared, 1);
assert.equal(lookupReasoning("call_api_provider_ds"), null);
assert.equal(lookupReasoning("call_api_provider_kimi"), "Keep provider");
});
});