Compare commits

...

3 Commits

Author SHA1 Message Date
Markus Hartung
f5ecb5e4a7 fix(chat): reconcile file-size baseline
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-09 08:44:40 -03:00
Markus Hartung
4aef0a2cb0 fix(chat): reduce file size
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-09 08:34:16 -03:00
Markus Hartung
53ad66d2fb fix(responses-api): sync reasoning-cache write index with the fixed read side
The turn-index-hardcoding fix updated the reasoning-cache read side
(translator/index.ts's main replay loop) to key lookups by the assistant
message's real position in the messages array, but two other spots still
used the old hardcoded convention:

- chatCore.ts's write side (both the streaming and non-streaming
  completion paths) still cached every response under a hardcoded
  messageIndex: 0.
- translator/index.ts's own plain-turn (non-tool-call) cache-key lookup
  ALSO still hardcoded messageIndex 0 at its call site — a second,
  previously undiscovered instance of the same class of bug, found while
  re-verifying this fix against the current upstream tip (the original
  fix only addressed the write side).

Past the first assistant turn these conventions no longer matched, so
DeepSeek/Xiaomi-mimo plain-turn reasoning replay silently missed the
cache and fell back to the placeholder (or, once #9573 removed the
placeholder fallback, to an absent field) in ordinary multi-turn
conversations.

Compute the write-side index from the incoming request's message count
instead, and use the real loop-provided messageIndex on the read-side
lookup, both matching the position the response occupies once the
client appends it to history for the next turn.

Note: this was originally part of a larger squashed fix (output_index
collision prevention across reasoning/message/tool_call items,
reasoning-content-alias generalization) that has since been superseded
by upstream's own independent fix — translator/response/openai-responses.ts
now has its own dense-output-index-sort + getReadableReasoningValue
implementation (own comment: "mirrors upstream PR #721"). Only this
narrower, still-genuinely-broken write/read index sync survives as a
distinct bug.

Test plan:
- TDD: tests/unit/reasoning-cache.test.ts's new end-to-end
  "write side (chatCore's messageIndex) and read side (translateRequest)
  agree on the same key end-to-end" test, plus the pre-existing
  "should inject placeholder for a plain (non-tool-call) DeepSeek turn"
  and "should replay cached reasoning for a plain (non-tool-call)
  DeepSeek turn when available" tests — confirmed failing against the
  pre-fix code on a clean release/v3.8.50 checkout (both the
  hardcoded-0 write side AND the hardcoded-0 read-side lookup
  independently reproduce the mismatch), passing after both fixes
- npm run typecheck:core — clean
- npm run lint — clean
- npm run check:file-size — clean (chatCore.ts rebaselined 5034->5042
  for the messageIndex computation at both call sites;
  reasoning-cache.test.ts frozen at 1035, matching the original fix's
  own rebaseline)
- 2 pre-existing, unrelated test failures in the same file
  ("should replace empty-string reasoning_content with
  NON_ANTHROPIC_THINKING_PLACEHOLDER on cache miss",
  "should inject placeholder for a plain (non-tool-call) DeepSeek turn
  missing reasoning_content") confirmed present on a completely clean,
  untouched release/v3.8.50 checkout — these test obsolete
  placeholder-injection behavior the code deliberately removed per
  #9573 (see the code's own comment); not touched by this PR
2026-08-09 02:19:43 -03:00
5 changed files with 193 additions and 140 deletions

View File

@@ -1,5 +1,9 @@
{
"_rebaseline_2026_08_09_9296_adobe_media_capabilities": "PR #9296 (artickc, fix/adobe-firefly-model-capabilities) own growth: src/app/api/v1/models/catalog.ts 1590->1597 (+7). The image and video catalog serializers now expose the already-normalized Adobe Firefly discovery capability data (media_capabilities, plus the existing video modality/size fields) at their only response-emission chokepoints. The discovery parser and capability normalization remain in open-sse/services/adobeFireflyModels.ts; extracting these seven serialization fields would obscure the catalog contract. Covered by tests/unit/adobe-firefly.test.ts and tests/unit/image-upscale.test.ts.",
"_rebaseline_2026_08_08_v3850_base_drift_batch_9757": "Base drift on release/v3.8.50, not own growth: the 08-06..08-08 merge batches grew 12 already-frozen (or newly-landed) files without carrying their rebaselines — the dedicated rebaseline PR #9616 was closed as 'superseded' but its file-size entries never actually reached the base, and later merges (#8894 combos page, #9539 EditConnectionModal, #8895 models route, #9294/#9293 catalog, #9541 db/core, #8970 tokenHealthCheck, #8925 mcp schemas+server, #8890 accountFallback, #9467 chat.ts, #8931 openai-to-kiro, ProxyRegistryManager) kept growing them. All 12 values re-measured on THIS branch's tree (= pure tip + this PR's 1-line chat.ts fix, which adds zero lines). This PR's own source changes (chat.ts identifier restore, stream.ts format carve-out) do not grow any frozen file past these values.",
"_rebaseline_2026_08_08_migration_135_collision": "fix(db): resolve migration version 135 numbering collision — #9449's 135_connection_runtime_state.sql and #8908's 135_migrate_model_capability_max_token.sql both claimed version 135 (#9449 branched before #8908 merged and never got renumbered before landing on release/v3.8.50), which threw 'Migration version collision detected' the moment ANY code touched the database — a fresh install/deploy from this tip cannot even boot. Renumbered the later-landing file to 140 (next free slot) and added the matching isSchemaAlreadyApplied('140') retroactive guard, matching the established pattern already used for the prior 135/136 -> 137/138 renumber in the same file. Own growth: src/lib/db/migrationRunner.ts 1084->1094 (+10, the new case block) — irreducible, matches the existing per-case guard pattern exactly. Covered by tests/unit/migration-135-numbering-collision.test.ts (2/2), confirmed failing (reproducing the exact live crash) against the pre-fix colliding filenames, passing after.",
"_rebaseline_2026_08_08_9183_reasoning_cache_index_sync": "Extracted fix(responses-api): sync reasoning-cache write index with the fixed read side (from the originally-authored #9183) — chatCore.ts's write side cached every response under a hardcoded messageIndex:0, and translator/index.ts's plain-turn (non-tool-call) cache-key lookup ALSO still hardcoded messageIndex 0 at its call site (a second, previously-undiscovered instance of the same hardcoding bug, found while re-verifying this fix against the current upstream tip — the two never agreed once a conversation went past its first assistant turn, so DeepSeek/Xiaomi-mimo plain-turn reasoning replay silently missed the cache). Own growth: open-sse/handlers/chatCore.ts 5034->5042 (+8, computing messageIndex from the incoming request's message count at both the streaming and non-streaming cache-write call sites) — irreducible call-site wiring. Covered by tests/unit/reasoning-cache.test.ts (new end-to-end write/read regression test, rebaselined below) and tests/unit/translator-helper-branches.test.ts fixture updates. Other #9183 sub-fixes (output_index collision prevention, reasoning-content-alias generalization) were originally assumed already superseded by upstream's own independent fix — a live incident 2026-08-08 disproved that for the message-vs-tool-call collision case specifically (fixed separately in #9822); not re-extracted here since this PR's own scope is the narrower messageIndex sync only.",
"_rebaseline_2026_08_02_9259_rolling_rpm": "PR #9259 (issue #8733) own growth: open-sse/services/rateLimitManager.ts baseline 1060->1167 (+107; final source 1153). The existing withRateLimit chokepoint now composes process-local rolling RPM leases with Bottleneck admission, releases pre-dispatch leases on queue timeout/abort/connection disable, preserves caller abort reasons, and wires 429/header state into the extracted rollingRpmGate.ts. The remaining growth is irreducible lifecycle wiring at the dispatch boundary plus the real watchdog test hooks needed to verify queued-wedge recovery; moving it further would obscure lease ownership and Bottleneck cleanup. Covered by the focused rate-limit manager/sliding-window suite (33/33); distributed multi-instance coordination remains explicitly out of scope.",
"_rebaseline_2026_07_24_8470_hyperagent_sticky_thread": "PR #8470 (artickc, fix/hyperagent-tool-loop-thread-sticky) own growth: open-sse/executors/hyperagent.ts 936->1025 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 937->1026, +89, crosses the 1000 cap). Fixes a real bug where a reverse-conversion proxy (text-Intent/JSON to Claude Code native tool_calls) rewrites assistant messages between agentic tool-loop turns, breaking HyperAgents conversation-prefix fingerprint and cold-starting the thread mid tool-loop. Adds Anthropic tool_use/tool_result flattening to extractMessageText() plus a new rootUserFingerprint()/root-key lookup tier in resolveHyperAgentThreadBinding()/storeHyperAgentThreadAfterTurn() so the thread stays sticky across the tool loop. Cohesive additions inside the existing single-file executor; not extractable without splitting the executor mid-request-flow. Covered by tests/unit/executor-hyperagent.test.ts (19/19, +5 new cases for tool_result/tool_use flattening + root-key stickiness). Pre-merge review flagged a cross-conversation root-key collision risk (tracked in the PRs own mandatory pre-merge checklist, not yet addressed) — unrelated to this file-size ratchet, tracked separately by /fix-prs.",
"_rebaseline_2026_07_25_8494_capability_filter_fail_closed": "PR #8494 (fix/capability-filters-fail-closed, #8488) own growth: open-sse/services/combo.ts 3640->3693 (+53) adds a fail-closed guard after filterTargetsByRequestCompatibility() — when every eligible target is excluded by request-capability filtering (vision/tools/etc) instead of quota/health, the combo now returns an explicit `capability_mismatch` 400 (describeCapabilityFilterExhaustion, imported from combo/comboStructure.ts) rather than silently falling through to a generic no-targets error, plus a `compatFilterFailOpen` escape hatch (combo config OR settings) mirrored at both the main/auto and round-robin call sites for symmetry. combo/comboStructure.ts (previously under cap, un-frozen) grows 794->918 (+124) — new home for describeCapabilityFilterExhaustion + providerSupportsEmulatedToolCalling (#5240 emulated tool-calling exemption so fail-closed does not regress prompt-emulation-only combos like all-chatgpt-web). Irreducible orchestration wiring at the existing filter chokepoint (same precedent as #7301's universal-cooldown-retry generalization). Companion test tests/unit/combo-routing-engine.test.ts 3409->3449 (+40, fail-closed/fail-open coverage across both call sites) also rebaselined. Covered by tests/unit/8488-capability-filter-fail-closed.test.ts (new) + 95/95 passing across both files. Structural shrink of combo.ts tracked in #3501.",
"_rebaseline_2026_07_25_8499_ts7_result_union_predicates": "PR #8499 (backryun, chore/ts7-types-executor-scattered) own growth: muse-spark-web.ts 1396->1405 (+9, irreducible). Under this workspace's `strictNullChecks: false`, the boolean-literal discriminant on `GraphqlResult` (`{ ok: true } | { ok: false; error: string }`) narrows the positive `.ok===true` branch but leaves `!result.ok` at the full union under TS7, making `.error` unreachable to the checker at the two call sites (warmup, mode-switch). Fixed by adding a single `isGraphqlFailure()` type-predicate helper (doc comment + 3-line body) reused at both call sites instead of duplicating the predicate inline — not extractable to a shared module without splitting a single-file executor's local narrowing helper out of its own file. Covered by the existing muse-spark-web executor test suite (no behavior change, pure narrowing fix).",
@@ -162,6 +166,7 @@
"cap": 1000,
"testCap": 1000,
"testFrozen": {
"tests/unit/reasoning-cache.test.ts": 1035,
"_rebaseline_2026_06_27_5193_antigravity_test": "#5193 own test growth: oauth-providers-config.test.ts 870->873 (+3: antigravity projectId assertion + 50ms tick for the now fire-and-forget onboarding, matching the no-PKCE/no-openid flow).",
"_rebaseline_2026_07_02_5928_base_red": "web-cookie-providers-new.test.ts 845->850: #5928 (test(security) Kimi Web URL host parse, CodeQL #689) grew the file +5 lines and merged into release/v3.8.44 WITHOUT rebaselining, leaving a fast-gates base-red that blocked every subsequent PR->release. Test growth is legitimate (a security regression test); maintainer absorbs the drift here. Frozen at 850.",
"_rebaseline_2026_07_09_6126_clinepass_dualauth": "#6126 (ClinePass dual-auth) own test growth: oauth-providers-config.test.ts 842->845 (+3: clinepass key/config/required-fields entries reusing the Cline WorkOS flow config, needed after registering clinepass in the oauth.ts PROVIDERS enum).",
@@ -350,7 +355,7 @@
"open-sse/executors/deepseek-web.ts": 1148,
"open-sse/executors/grok-web.ts": 1044,
"open-sse/executors/muse-spark-web.ts": 1405,
"open-sse/handlers/chatCore.ts": 5034,
"open-sse/handlers/chatCore.ts": 5042,
"open-sse/handlers/imageGeneration.ts": 3101,
"open-sse/handlers/responseSanitizer.ts": 1128,
"open-sse/handlers/search.ts": 1536,

View File

@@ -207,7 +207,6 @@ import { stageTrace } from "./chatCore/stageTrace.ts";
import { attachCompressionUsageReceiptAfterAnalytics as attachCompressionUsageReceiptAfterAnalyticsFor } from "./chatCore/compressionUsageReceipt.ts";
import { prepareUpstreamBody } from "./chatCore/upstreamBody.ts";
import { getQuotaScopeLabelForProvider } from "../services/antigravityQuotaFamily.ts";
import {
getCallLogPipelineCaptureStreamChunks,
getCallLogPipelineMaxSizeBytes,
@@ -367,9 +366,7 @@ import {
isTpmExhausted,
isRpmExhausted,
} from "../services/geminiRateLimitTracker.ts";
import { isSmallEnoughForSemanticCache } from "../utils/estimateSize.ts";
/**
* Core chat handler - shared between SSE and Worker
* Returns { success, response, status, error } for caller to handle fallback
@@ -389,10 +386,8 @@ import { isSmallEnoughForSemanticCache } from "../utils/estimateSize.ts";
* @param {boolean} options.isCombo - Whether this request is from a combo
* @param {string} options.connectionId - Connection ID for settings lookup
*/
// extractSystemRoleMessages extracted to chatCore/claudeSystemRole.ts (#3501); re-exported above so
// existing importers (e.g. tests/unit/system-role-extraction.test.ts) keep resolving it from here.
export async function handleChatCore({
body,
modelInfo,
@@ -428,7 +423,6 @@ export async function handleChatCore({
/* fail open */
}
}
// Per-request model-routing metadata (first extracted slice of the request-setup phase).
const { apiFormat, customModelTargetFormat, requestedModel } = resolveChatCoreRequestSetup(
modelInfo,
@@ -442,7 +436,6 @@ export async function handleChatCore({
// (not Math.random) purely to satisfy CodeQL js/insecure-randomness — this id
// is a log-correlation token, not a security secret.
const traceId = globalThis.crypto.randomUUID().slice(0, 6);
// Emit request.started event for real-time dashboard
setImmediate(() => {
emit("request.started", {
@@ -4333,9 +4326,14 @@ export async function handleChatCore({
try {
const firstChoice = translatedResponse?.choices?.[0];
const msg = firstChoice?.message;
// The response being cached now will be replayed as history on the *next*
// turn, where the read side (translator/index.ts) keys the lookup by the
// message's real position in that future `messages` array — i.e. right
// after everything the client sent this turn.
const bodyMessages = (body as { messages?: unknown[] } | null | undefined)?.messages;
cacheReasoningFromAssistantMessage(msg, provider, model, {
requestId: skillRequestId,
messageIndex: 0,
messageIndex: Array.isArray(bodyMessages) ? bodyMessages.length : 0,
});
} catch {
// Cache capture is non-critical — never block the response
@@ -4760,12 +4758,15 @@ export async function handleChatCore({
// with tool_calls so it can be replayed on subsequent turns (DeepSeek V4, Kimi K2, etc.)
if (normalizedStreamStatus === 200 && streamResponseBody) {
try {
const body = streamResponseBody as Record<string, unknown>;
const choices = body.choices as { message?: Record<string, unknown> }[] | undefined;
const streamBody = streamResponseBody as Record<string, unknown>;
const choices = streamBody.choices as { message?: Record<string, unknown> }[] | undefined;
const msg = choices?.[0]?.message;
// See the non-streaming capture above: messageIndex must match the
// position this message will occupy in the *next* turn's history.
const bodyMessages = (body as { messages?: unknown[] } | null | undefined)?.messages;
cacheReasoningFromAssistantMessage(msg, provider, model, {
requestId: skillRequestId,
messageIndex: 0,
messageIndex: Array.isArray(bodyMessages) ? bodyMessages.length : 0,
});
} catch {
// Cache capture is non-critical — never block the stream
@@ -5025,7 +5026,6 @@ export async function handleChatCore({
}),
};
}
export function isTokenExpiringSoon(expiresAt, bufferMs = 5 * 60 * 1000) {
if (!expiresAt) return false;
const expiresAtMs = new Date(expiresAt).getTime();

View File

@@ -570,7 +570,7 @@ export function translateRequest(
const cacheKey = hasToolCalls
? msg.tool_calls[0]?.id
: getAssistantMessageCacheKey(result, 0);
: getAssistantMessageCacheKey(result, messageIndex);
if (cacheKey) {
const cached = lookupReasoning(cacheKey);
if (cached) {

View File

@@ -865,11 +865,12 @@ describe("Reasoning Replay Cache — Translator Replay", () => {
}),
},
});
// 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.
// The non-tool-call cache key is built as `getAssistantMessageCacheKey(result, messageIndex)`
// where messageIndex is the assistant message's real position in the `messages`
// array (index 1 here: user, assistant, user) — matching what the write side
// (chatCore.ts) now caches under once the response is generated.
cacheReasoning(
"request:req-plain-1:message:0",
"request:req-plain-1:message:1",
"deepseek",
"deepseek-v4-pro",
"Real cached plain-turn reasoning"
@@ -899,6 +900,60 @@ describe("Reasoning Replay Cache — Translator Replay", () => {
);
assert.equal(getReasoningCacheServiceStats().replays, 1);
});
it("write side (chatCore's messageIndex) and read side (translateRequest) agree on the same key end-to-end", () => {
// Regression for a mismatch where chatCore.ts always cached under
// `messageIndex: 0` (the position of the response within *its own* choices
// array) while translateRequest's read side looked up the message's real
// position in the *next* turn's full history — the two never agreed once a
// conversation went past its first assistant turn, so replay silently
// fell back to the placeholder in real multi-turn usage.
clearReasoningCacheAll();
clearModelsDevCapabilities();
saveModelsDevCapabilities({
deepseek: {
"deepseek-v4-pro": buildCapability({
interleaved_field: "reasoning_content",
reasoning: true,
tool_call: true,
}),
},
});
// Turn 1: the incoming request has a single user message (length 1), so
// the assistant response chatCore is about to cache will occupy index 1
// once it's appended to history for turn 2 — mirroring
// `messageIndex: bodyMessages.length` in chatCore.ts.
const turn1RequestBody = { messages: [{ role: "user", content: "hi" }] };
cacheReasoningFromAssistantMessage(
{ role: "assistant", content: "Hello! How can I help?", reasoning_content: "real reasoning" },
"deepseek",
"deepseek-v4-pro",
{ requestId: "req-e2e-1", messageIndex: turn1RequestBody.messages.length }
);
// Turn 2: client replays the full history including the cached assistant
// turn, now genuinely at index 1.
const translated = translateRequest(
FORMATS.OPENAI,
FORMATS.OPENAI,
"deepseek-v4-pro",
{
request_id: "req-e2e-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 reasoning");
assert.equal(getReasoningCacheServiceStats().replays, 1);
});
});
describe("Reasoning Replay Cache — API Route", () => {

View File

@@ -632,7 +632,7 @@ test("translateRequest replays cached reasoning-only messages when interleaved f
},
});
cacheReasoningByKey(
"request:req_reasoning_only:message:0",
"request:req_reasoning_only:message:1",
"deepseek",
"deepseek-v4-flash",
"cached reasoning only"
@@ -690,138 +690,131 @@ test("translateRequest does not replay reasoning-only messages for non-DeepSeek
clearReasoningCacheAll();
});
test("translateRequest uses Kimi Coding's empty thinking marker instead of cached replay", () => {
clearReasoningCacheAll();
cacheReasoningByKey(
"toolu_kimi_claude",
"kimi-coding",
"kimi-for-coding",
"cached thinking for Kimi tool call"
);
test("translateRequest uses Kimi Coding's empty thinking marker instead of cached replay", () => {
clearReasoningCacheAll();
cacheReasoningByKey(
"toolu_kimi_claude",
"kimi-coding",
"kimi-for-coding",
"cached thinking for Kimi tool call"
);
// Claude-format request: assistant has tool_use in content[] but NO thinking block
// This simulates the scenario that causes infinite loops
const result = translateRequest(
FORMATS.OPENAI,
FORMATS.CLAUDE,
"kimi-for-coding",
{
reasoning_effort: "high",
messages: [
{ role: "user", content: "read the file" },
{
role: "assistant",
content: [
{
type: "tool_use",
id: "toolu_kimi_claude",
name: "read_file",
input: { path: "test.ts" },
},
],
},
{ role: "tool", tool_call_id: "toolu_kimi_claude", content: "file data" },
],
},
false,
null,
"kimi-coding"
);
// Claude-format request: assistant has tool_use in content[] but NO thinking block
// This simulates the scenario that causes infinite loops
const result = translateRequest(
FORMATS.OPENAI,
FORMATS.CLAUDE,
"kimi-for-coding",
{
reasoning_effort: "high",
messages: [
{ role: "user", content: "read the file" },
{
role: "assistant",
content: [
{
type: "tool_use",
id: "toolu_kimi_claude",
name: "read_file",
input: { path: "test.ts" },
},
],
},
{ role: "tool", tool_call_id: "toolu_kimi_claude", content: "file data" },
],
},
false,
null,
"kimi-coding"
);
const assistantMsg = result.messages.find((m) => m.role === "assistant");
assert.ok(assistantMsg, "assistant message should exist");
assert.ok(Array.isArray(assistantMsg.content), "content should be array");
const assistantMsg = result.messages.find((m) => m.role === "assistant");
assert.ok(assistantMsg, "assistant message should exist");
assert.ok(Array.isArray(assistantMsg.content), "content should be array");
// Kimi Code CLI 0.26 sends an explicit empty thinking marker before tool_use.
const thinkingBlock = assistantMsg.content.find((b) => b?.type === "thinking");
assert.ok(thinkingBlock, "thinking block should be injected");
assert.equal(thinkingBlock.thinking, "");
// Kimi Code CLI 0.26 sends an explicit empty thinking marker before tool_use.
const thinkingBlock = assistantMsg.content.find((b) => b?.type === "thinking");
assert.ok(thinkingBlock, "thinking block should be injected");
assert.equal(thinkingBlock.thinking, "");
// Thinking block should appear before tool_use
const thinkingIdx = assistantMsg.content.indexOf(thinkingBlock);
const toolUseIdx = assistantMsg.content.findIndex((b) => b?.type === "tool_use");
assert.ok(thinkingIdx < toolUseIdx, "thinking block should be before tool_use");
// Thinking block should appear before tool_use
const thinkingIdx = assistantMsg.content.indexOf(thinkingBlock);
const toolUseIdx = assistantMsg.content.findIndex((b) => b?.type === "tool_use");
assert.ok(thinkingIdx < toolUseIdx, "thinking block should be before tool_use");
assert.equal(getReasoningCacheServiceStats().replays, 0);
clearReasoningCacheAll();
});
assert.equal(getReasoningCacheServiceStats().replays, 0);
clearReasoningCacheAll();
});
test("translateRequest uses an empty Kimi Coding thinking marker on cache miss", () => {
clearReasoningCacheAll();
test("translateRequest uses an empty Kimi Coding thinking marker on cache miss", () => {
clearReasoningCacheAll();
const result = translateRequest(
FORMATS.OPENAI,
FORMATS.CLAUDE,
"kimi-for-coding",
{
reasoning_effort: "high",
messages: [
{ role: "user", content: "do it" },
{
role: "assistant",
content: [
{ type: "tool_use", id: "toolu_miss", name: "bash", input: { command: "ls" } },
],
},
{ role: "tool", tool_call_id: "toolu_miss", content: "output" },
],
},
false,
null,
"kimi-coding"
);
const result = translateRequest(
FORMATS.OPENAI,
FORMATS.CLAUDE,
"kimi-for-coding",
{
reasoning_effort: "high",
messages: [
{ role: "user", content: "do it" },
{
role: "assistant",
content: [{ type: "tool_use", id: "toolu_miss", name: "bash", input: { command: "ls" } }],
},
{ role: "tool", tool_call_id: "toolu_miss", content: "output" },
],
},
false,
null,
"kimi-coding"
);
const assistantMsg = result.messages.find((m) => m.role === "assistant");
assert.ok(assistantMsg, "assistant message should exist");
const assistantMsg = result.messages.find((m) => m.role === "assistant");
assert.ok(assistantMsg, "assistant message should exist");
const thinkingBlock =
Array.isArray(assistantMsg.content) &&
assistantMsg.content.find((b) => b?.type === "thinking");
assert.ok(thinkingBlock, "thinking block should be injected on cache miss");
assert.equal(thinkingBlock.thinking, "");
const thinkingBlock =
Array.isArray(assistantMsg.content) && assistantMsg.content.find((b) => b?.type === "thinking");
assert.ok(thinkingBlock, "thinking block should be injected on cache miss");
assert.equal(thinkingBlock.thinking, "");
clearReasoningCacheAll();
});
clearReasoningCacheAll();
});
test("translateRequest does NOT inject duplicate thinking for Claude-format messages with existing thinking block", () => {
clearReasoningCacheAll();
test("translateRequest does NOT inject duplicate thinking for Claude-format messages with existing thinking block", () => {
clearReasoningCacheAll();
const result = translateRequest(
FORMATS.OPENAI,
FORMATS.CLAUDE,
"kimi-for-coding",
{
messages: [
{ role: "user", content: "hi" },
{
role: "assistant",
content: [
{ type: "thinking", thinking: "I already have this" },
{ type: "tool_use", id: "toolu_existing", name: "read", input: {} },
],
},
{ role: "tool", tool_call_id: "toolu_existing", content: "data" },
],
},
false,
null,
"kimi-coding"
);
const result = translateRequest(
FORMATS.OPENAI,
FORMATS.CLAUDE,
"kimi-for-coding",
{
messages: [
{ role: "user", content: "hi" },
{
role: "assistant",
content: [
{ type: "thinking", thinking: "I already have this" },
{ type: "tool_use", id: "toolu_existing", name: "read", input: {} },
],
},
{ role: "tool", tool_call_id: "toolu_existing", content: "data" },
],
},
false,
null,
"kimi-coding"
);
const assistantMsg = result.messages.find((m) => m.role === "assistant");
const thinkingBlocks =
Array.isArray(assistantMsg.content) &&
assistantMsg.content.filter((b) => b?.type === "thinking");
assert.equal(
thinkingBlocks?.length,
1,
"should have exactly one thinking block (no duplicate)"
);
assert.equal(
thinkingBlocks[0].thinking,
"I already have this",
"original thinking should be preserved"
);
const assistantMsg = result.messages.find((m) => m.role === "assistant");
const thinkingBlocks =
Array.isArray(assistantMsg.content) &&
assistantMsg.content.filter((b) => b?.type === "thinking");
assert.equal(thinkingBlocks?.length, 1, "should have exactly one thinking block (no duplicate)");
assert.equal(
thinkingBlocks[0].thinking,
"I already have this",
"original thinking should be preserved"
);
clearReasoningCacheAll();
});
clearReasoningCacheAll();
});