fix(models): treat unknown output caps as unset (#4584)

Integrated into release/v3.8.34
This commit is contained in:
Randi
2026-06-22 16:05:47 -04:00
committed by GitHub
parent 5887bc3d6a
commit 67c8a0436e
21 changed files with 206 additions and 106 deletions

View File

@@ -795,6 +795,11 @@ KIRO_USER_AGENT="AWS-SDK-JS/3.0.0 kiro-ide/1.0.0"
# rather than a secret. Only override if AWS ever starts enforcing this field.
# Used by: src/lib/oauth/constants/oauth.ts (KIRO_CONFIG.socialClientId).
# KIRO_OAUTH_CLIENT_ID=kiro-cli
# Enable full per-frame message CRC validation for Kiro streams. Off by default
# because it is O(frame bytes) on the main thread; use only for debugging
# suspected corrupted-stream issues.
# Used by: open-sse/executors/kiro.ts
# KIRO_VERIFY_FULL_CRC=false
QODER_USER_AGENT="Qoder-Cli"
QWEN_USER_AGENT="QwenCode/0.15.11 (linux; x64)"
CURSOR_USER_AGENT="Cursor/3.4"

View File

@@ -2,9 +2,9 @@
## [Unreleased]
### 🐛 Fixed
### Fixed
- **fix(resilience): upstream retry hints toggle now also governs reset text in error bodies** — quota-reset text such as Antigravity `Resets in 160h...` and subscription quota timestamps no longer bypass the Settings → Resilience "Use upstream retry hints" switch. When the switch is disabled, those responses keep the local cooldown behavior instead of applying the upstream-provided reset window.
- **fix(models): unknown max output limits no longer default to 8192** — Models without synced, registry, or static `maxOutputTokens` metadata now resolve the limit as unknown instead of falling back to a generic 8192-token cap. Combo compatibility filtering, reasoning max-token buffering, and Claude/Gemini request translation only clamp or inject `maxOutputTokens` when a real model cap is known; otherwise requested output limits are preserved and absent limits stay absent.
---

View File

@@ -478,6 +478,14 @@ function deriveRequestCompatibilityRequirements(
};
}
function exceedsKnownOutputLimit(
requestedOutputTokens: number,
maxOutputTokens: number | null
): boolean {
if (requestedOutputTokens <= 0 || maxOutputTokens === null) return false;
return maxOutputTokens < requestedOutputTokens;
}
function getTargetCompatibilityFailures(
target: ResolvedComboTarget,
requirements: RequestCompatibilityRequirements
@@ -506,11 +514,7 @@ function getTargetCompatibilityFailures(
failures.push("structured_output");
}
if (
requirements.requestedOutputTokens > 0 &&
Number.isFinite(capabilities.maxOutputTokens) &&
capabilities.maxOutputTokens < requirements.requestedOutputTokens
) {
if (exceedsKnownOutputLimit(requirements.requestedOutputTokens, capabilities.maxOutputTokens)) {
failures.push("output_tokens");
}

View File

@@ -1,7 +1,4 @@
import { getResolvedModelCapabilities } from "../../src/lib/modelCapabilities.ts";
import { MODEL_SPECS } from "../../src/shared/constants/modelSpecs.ts";
const DEFAULT_MAX_OUTPUT_TOKENS = MODEL_SPECS.__default__.maxOutputTokens;
export function toPositiveInteger(value: unknown): number | null {
const numericValue =
@@ -29,7 +26,7 @@ export function resolveReasoningBufferedMaxTokens(
if (capabilities.supportsThinking !== true) return null;
const maxOutputTokens = toPositiveInteger(capabilities.maxOutputTokens);
if (maxOutputTokens === null || maxOutputTokens === DEFAULT_MAX_OUTPUT_TOKENS) return null;
if (maxOutputTokens === null) return null;
if (current > maxOutputTokens) return maxOutputTokens;
if (current === maxOutputTokens) return current;

View File

@@ -55,7 +55,10 @@ export function claudeToGeminiRequest(model, body, stream, credentials = null) {
result.generationConfig.topK = body.top_k;
}
if (body.max_tokens !== undefined) {
result.generationConfig.maxOutputTokens = capMaxOutputTokens(model, body.max_tokens);
const maxOutputTokens = capMaxOutputTokens(model, body.max_tokens);
if (maxOutputTokens !== null) {
result.generationConfig.maxOutputTokens = maxOutputTokens;
}
}
// ── System instruction ─────────────────────────────────────────

View File

@@ -41,14 +41,13 @@ function applyCopilotSummarizedThinkingDisplay(
// - max_tokens must be <= model output cap (e.g. 128000 for Opus 4.7)
const MIN_CLAUDE_THINKING_BUDGET = 1024;
const MIN_RESPONSE_ROOM = 1024;
const FALLBACK_OUTPUT_CAP = 128000;
function safeCapMaxOutputTokens(model: string): number {
function safeCapMaxOutputTokens(model: string): number | null {
try {
const cap = capMaxOutputTokens(model);
return typeof cap === "number" && cap > 0 ? cap : FALLBACK_OUTPUT_CAP;
return typeof cap === "number" && cap > 0 ? cap : null;
} catch {
return FALLBACK_OUTPUT_CAP;
return null;
}
}
@@ -86,26 +85,35 @@ export function fitThinkingToMaxTokens(
// No budgeted thinking — just cap max_tokens to the model output ceiling.
if (!thinking || requestedBudget <= 0) {
return {
maxTokens: Math.min(Math.max(callerMaxTokens, 1), modelCap),
maxTokens:
modelCap === null
? Math.max(callerMaxTokens, 1)
: Math.min(Math.max(callerMaxTokens, 1), modelCap),
thinking,
};
}
let responseRoom = Math.max(callerMaxTokens, MIN_RESPONSE_ROOM);
let target = Math.min(responseRoom + requestedBudget, modelCap);
let target =
modelCap === null
? responseRoom + requestedBudget
: Math.min(responseRoom + requestedBudget, modelCap);
let fittedBudget = target - responseRoom;
// If the cap squeezed thinking below Anthropic's floor, try shrinking
// response room to MIN_RESPONSE_ROOM to recover budget.
if (fittedBudget < MIN_CLAUDE_THINKING_BUDGET && responseRoom > MIN_RESPONSE_ROOM) {
responseRoom = MIN_RESPONSE_ROOM;
target = Math.min(responseRoom + requestedBudget, modelCap);
target =
modelCap === null
? responseRoom + requestedBudget
: Math.min(responseRoom + requestedBudget, modelCap);
fittedBudget = target - responseRoom;
}
// Cap too tight for any thinking — disable rather than send an invalid request.
if (fittedBudget < MIN_CLAUDE_THINKING_BUDGET) {
return { maxTokens: modelCap, thinking: undefined };
return { maxTokens: modelCap ?? Math.max(callerMaxTokens, 1), thinking: undefined };
}
const adjustedThinking: Record<string, unknown> = { ...thinking };

View File

@@ -32,9 +32,8 @@ import {
import { buildGeminiTools, sanitizeGeminiToolName } from "../helpers/geminiToolsSanitizer.ts";
// Observed Antigravity wrapper output cap, not an underlying model capability.
// Keep this bridge-local: capMaxOutputTokens() falls back to OmniRoute's generic
// 8192 default for unknown Claude-family IDs, while Antigravity currently caps
// visible output around 16K. See: https://github.com/keisksw/antigravity-output-analysis
// Keep this bridge-local: Antigravity currently caps visible output around 16K.
// See: https://github.com/keisksw/antigravity-output-analysis
const ANTIGRAVITY_CLAUDE_MAX_OUTPUT_TOKENS = 16_384;
type GeminiPart = Record<string, unknown>;
@@ -274,13 +273,12 @@ function openaiToGeminiBase(
if (body.stop !== undefined) {
result.generationConfig.stopSequences = Array.isArray(body.stop) ? body.stop : [body.stop];
}
const requestedMaxOutputTokens = (body.max_tokens ?? body.max_completion_tokens) as
| number
| undefined;
if (requestedMaxOutputTokens !== undefined) {
result.generationConfig.maxOutputTokens = capMaxOutputTokens(model, requestedMaxOutputTokens);
} else {
result.generationConfig.maxOutputTokens = capMaxOutputTokens(model);
const maxOutputTokens = capMaxOutputTokens(
model,
(body.max_tokens ?? body.max_completion_tokens) as number | undefined
);
if (maxOutputTokens !== null) {
result.generationConfig.maxOutputTokens = maxOutputTokens;
}
// Thinking / Reasoning support (Google Gemini 2.0+ Thinking models)

View File

@@ -53,7 +53,7 @@ export interface ResolvedModelCapabilities {
temperature: boolean | null;
contextWindow: number | null;
maxInputTokens: number | null;
maxOutputTokens: number;
maxOutputTokens: number | null;
defaultThinkingBudget: number;
thinkingBudgetCap: number | null;
thinkingOverhead: number | null;
@@ -382,7 +382,7 @@ export function getResolvedModelCapabilities(input: CapabilityInput): ResolvedMo
synced?.limit_output ??
(typeof registryModel?.maxOutputTokens === "number" ? registryModel.maxOutputTokens : null) ??
spec?.maxOutputTokens ??
MODEL_SPECS.__default__.maxOutputTokens,
null,
defaultThinkingBudget: spec?.defaultThinkingBudget ?? 0,
thinkingBudgetCap: spec?.thinkingBudgetCap ?? null,
thinkingOverhead: spec?.thinkingOverhead ?? null,
@@ -416,9 +416,11 @@ export function supportsMaxTokens(input: CapabilityInput): boolean {
return getResolvedModelCapabilities(input).supportsMaxTokens;
}
export function capMaxOutputTokens(input: CapabilityInput, requested?: number): number {
export function capMaxOutputTokens(input: CapabilityInput, requested?: number): number | null {
const cap = getResolvedModelCapabilities(input).maxOutputTokens;
return requested ? Math.min(requested, cap) : cap;
const hasRequested = typeof requested === "number" && Number.isFinite(requested);
if (cap === null) return hasRequested ? requested : null;
return hasRequested ? Math.min(requested, cap) : cap;
}
export function getDefaultThinkingBudget(input: CapabilityInput): number {

View File

@@ -5,7 +5,7 @@
*/
export interface ModelSpec {
maxOutputTokens: number;
maxOutputTokens?: number;
contextWindow?: number;
defaultThinkingBudget?: number;
thinkingBudgetCap?: number;
@@ -457,9 +457,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
},
// Defaults
__default__: {
maxOutputTokens: 8192,
},
__default__: {},
};
export function getModelSpec(modelId: string): ModelSpec | undefined {
@@ -515,10 +513,12 @@ export function normalizeThinkingForModel<T extends Record<string, unknown>>(
return body;
}
export function capMaxOutputTokens(modelId: string, requested?: number): number {
export function capMaxOutputTokens(modelId: string, requested?: number): number | undefined {
const spec = getModelSpec(modelId);
const cap = spec?.maxOutputTokens ?? MODEL_SPECS.__default__.maxOutputTokens;
return requested ? Math.min(requested, cap) : cap;
const cap = spec?.maxOutputTokens;
const hasRequested = typeof requested === "number" && Number.isFinite(requested);
if (typeof cap !== "number") return hasRequested ? requested : undefined;
return hasRequested ? Math.min(requested, cap) : cap;
}
export function getDefaultThinkingBudget(modelId: string): number {

View File

@@ -711,15 +711,10 @@ test("chat pipeline applies Codex CLI fingerprint to OAuth responses requests",
assert.ok(headerOrder.indexOf("Accept") < headerOrder.indexOf("User-Agent"));
const bodyOrder = Object.keys(JSON.parse(call.bodyString));
assert.deepEqual(bodyOrder.slice(0, 7), [
"model",
"stream",
"input",
"instructions",
"store",
"reasoning",
"prompt_cache_key",
]);
assert.deepEqual(
bodyOrder.slice(0, 8),
"model stream input instructions store reasoning include prompt_cache_key".split(" ")
);
assert.equal(call.body.model, "gpt-5.5");
assert.equal(call.body.store, false);
assert.equal(

View File

@@ -60,12 +60,15 @@ test.after(async () => {
await harness.cleanup();
});
async function enableMemory(maxTokens = 400) {
async function enableMemory(
maxTokens = 400,
strategy: "recent" | "semantic" | "hybrid" = "recent"
) {
await settingsDb.updateSettings({
memoryEnabled: true,
memoryMaxTokens: maxTokens,
memoryRetentionDays: 30,
memoryStrategy: "recent",
memoryStrategy: strategy,
});
}
@@ -172,6 +175,7 @@ test("later requests inject retrieved memories into upstream messages", async ()
test("memory search ranks query-relevant memories first", async () => {
const apiKey = await seedApiKey();
await enableMemory(400, "hybrid");
await memoryTools.omniroute_memory_add.handler({
apiKeyId: apiKey.id,

View File

@@ -1,5 +1,5 @@
{
"max_tokens": 8192,
"max_tokens": 64000,
"messages": [
{
"content": [

View File

@@ -10,7 +10,6 @@
}
],
"generationConfig": {
"maxOutputTokens": 8192,
"temperature": 0.7,
"thinkingConfig": {
"includeThoughts": true,

View File

@@ -2978,8 +2978,8 @@ test("#3587 reasoning buffer is disabled without explicit model capability data"
);
assert.equal(
resolveReasoningBufferedMaxTokens("openai/default-cap-reasoning", 100),
null,
"default-sized caps are treated as unknown because registry fallbacks use the same value"
1100,
"explicit default-sized caps are treated as real capability data"
);
});

View File

@@ -423,30 +423,32 @@ test("reset-aware strategy avoids accounts near 5h exhaustion", async (t) => {
assert.equal(await selectedConnectionFor(combo), healthy5h.id);
});
test("reset-aware strategy rotates similar scores with round-robin tie breaking", async (t) => {
const first = { id: `first-${randomUUID()}`, token: `token-first-${randomUUID()}` };
const second = {
id: `second-${randomUUID()}`,
token: `token-second-${randomUUID()}`,
};
const reset5hAt = Math.floor((Date.now() + 2 * 3600 * 1000) / 1000);
const reset7dAt = Math.floor((Date.now() + 3 * 24 * 3600 * 1000) / 1000);
test("reset-aware strategy rotates similar scores with round-robin tie breaking", async () => {
const provider = `tie-provider-${randomUUID()}`;
const first = `first-${randomUUID()}`;
const second = `second-${randomUUID()}`;
const quota = {
rate_limit: {
primary_window: { used_percent: 50, reset_at: reset5hAt },
secondary_window: { used_percent: 50, reset_at: reset7dAt },
},
used: 50,
total: 100,
percentUsed: 0.5,
resetAt: "2099-01-01T00:00:00.000Z",
};
t.after(
installCodexQuotaMock({
[first.token]: quota,
[second.token]: quota,
})
);
const combo = resetAwareCombo(`reset-aware-rr-${randomUUID()}`, [first, second], {
resetAwareTieBandPercent: 100,
});
registerQuotaFetcher(provider, async () => quota);
const combo = {
name: `reset-aware-rr-${randomUUID()}`,
strategy: "reset-aware",
config: { resetAwareTieBandPercent: 100 },
models: [first, second].map((connectionId, index) => ({
kind: "model",
provider,
providerId: provider,
model: "balanced-model",
connectionId,
id: `tie-${index}`,
})),
};
const selections = [
await selectedConnectionFor(combo),
@@ -454,8 +456,8 @@ test("reset-aware strategy rotates similar scores with round-robin tie breaking"
await selectedConnectionFor(combo),
];
assert.equal(selections.includes(first.id), true);
assert.equal(selections.includes(second.id), true);
assert.equal(selections.includes(first), true);
assert.equal(selections.includes(second), true);
});
test("reset-aware strategy uses registered quota fetchers for non-Codex providers", async () => {

View File

@@ -41,10 +41,7 @@ test.after(() => {
// --- Part A: capability resolution -----------------------------------------
test("Pixtral resolves supportsVision=true via model-id heuristic (no synced data)", () => {
assert.equal(
getResolvedModelCapabilities("mistral/pixtral-12b-latest").supportsVision,
true
);
assert.equal(getResolvedModelCapabilities("mistral/pixtral-12b-latest").supportsVision, true);
});
test("a text-only Mistral model is NOT a vision false-positive", () => {
@@ -115,3 +112,14 @@ test("text-only request: targets are untouched by the vision filter", () => {
);
assert.equal(out.length, 1);
});
test("large output request: unknown maxOutputTokens does not filter a target", () => {
const out = filterTargetsByRequestCompatibility(
[target("openai-compatible-local/custom-large-output-model"), target("openai/gpt-4o-mini")],
{ messages: [{ role: "user", content: "hello" }], max_tokens: 32000 },
noopLog
);
const ids = out.map((t) => t.modelStr);
assert.deepEqual(ids, ["openai-compatible-local/custom-large-output-model"]);
});

View File

@@ -97,13 +97,12 @@ test.after(() => {
// ---------------------------------------------------------------------------
test("#3500 buildUnifiedSource — raw-only branch when sinceIso is recent", () => {
// A very recent sinceIso means no aggregated rows are needed
const recentIso = new Date(Date.now() - 3600_000).toISOString(); // 1 hour ago
const today = new Date().toISOString().split("T")[0];
// sinceIso >= rawCutoffDate means no aggregated rows are needed.
const recentIso = "2025-06-02T12:00:00.000Z";
const result = mod.buildUnifiedSource({
sinceIso: recentIso,
untilIso: null,
rawCutoffDate: today,
rawCutoffDate: "2025-06-01",
apiKeyWhere: "",
apiKeyParams: {},
});

View File

@@ -137,6 +137,27 @@ test("canonical model capability resolver lets exact synced metadata override gl
assert.equal(bareGpt55.contextWindow, 1050000);
});
test("unknown models keep maxOutputTokens null instead of using a generic default", () => {
const unknown = modelCapabilities.getResolvedModelCapabilities(
"openai-compatible-local/custom-large-output-model"
);
assert.equal(unknown.contextWindow, null);
assert.equal(unknown.maxInputTokens, null);
assert.equal(unknown.maxOutputTokens, null);
assert.equal(
modelCapabilities.capMaxOutputTokens(
"openai-compatible-local/custom-large-output-model",
32000
),
32000
);
assert.equal(
modelCapabilities.capMaxOutputTokens("openai-compatible-local/custom-large-output-model"),
null
);
});
test("GPT OSS and DeepSeek Reasoner models support tool calling", () => {
// GPT OSS models should not be blocked by the heuristic
assert.equal(modelCapabilities.supportsToolCalling("fake-provider/gpt-oss-120b"), true);

View File

@@ -121,6 +121,19 @@ test("Claude -> Gemini clamps maxOutputTokens to the model cap", () => {
assert.equal(result.generationConfig.maxOutputTokens, 65536);
});
test("Claude -> Gemini preserves requested maxOutputTokens when the model cap is unknown", () => {
const result = claudeToGeminiRequest(
"gemini-2.5-pro",
{
messages: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
max_tokens: 32000,
},
false
);
assert.equal(result.generationConfig.maxOutputTokens, 32000);
});
test("Claude -> Gemini converts text and base64 images to Gemini parts", () => {
const result = claudeToGeminiRequest(
"gemini-2.5-flash",

View File

@@ -256,7 +256,9 @@ test("OpenAI -> Claude maps tool_choice and injects response_format instructions
});
test("OpenAI -> Claude turns reasoning settings into thinking budgets and expands max tokens", () => {
// `claude-4-sonnet` is a fixture that doesn't match any spec → default cap = 8192.
// `claude-4-sonnet` is a fixture that doesn't match any spec. Unknown caps
// should not get an implicit default; the translator only preserves the
// response room + thinking budget relationship.
// fitThinkingToMaxTokens floors response room at MIN_RESPONSE_ROOM (1024)
// and targets max_tokens = responseRoom + budget capped at modelCap.
const effortResult = openaiToClaudeRequest(
@@ -270,7 +272,7 @@ test("OpenAI -> Claude turns reasoning settings into thinking budgets and expand
);
assert.deepEqual(effortResult.thinking, { type: "enabled", budget_tokens: 1024 });
// responseRoom=max(10,1024)=1024; target=min(1024+1024, 8192)=2048
// responseRoom=max(10,1024)=1024; target=1024+1024=2048
assert.equal(effortResult.max_tokens, 2048);
const explicitThinkingResult = openaiToClaudeRequest(
@@ -288,10 +290,25 @@ test("OpenAI -> Claude turns reasoning settings into thinking budgets and expand
budget_tokens: 2000,
max_tokens: 3000,
});
// responseRoom=max(1000,1024)=1024; target=min(1024+2000, 8192)=3024
// responseRoom=max(1000,1024)=1024; target=1024+2000=3024
assert.equal(explicitThinkingResult.max_tokens, 3024);
});
test("OpenAI -> Claude does not cap unknown models to a fallback maxOutputTokens", () => {
const result = openaiToClaudeRequest(
"claude-4-sonnet",
{
messages: [{ role: "user", content: "Reason about something hard" }],
max_tokens: 32000,
reasoning_effort: "high",
},
false
);
assert.equal(result.max_tokens, 163072);
assert.deepEqual(result.thinking, { type: "enabled", budget_tokens: 131072 });
});
test("OpenAI -> Claude preserves xhigh only for Claude models that expose it", () => {
const { xhighModel, standardModel } = getClaudeEffortFixtures();
const preserved = openaiToClaudeRequest(

View File

@@ -18,6 +18,8 @@ const { clearGeminiThoughtSignatures } =
await import("../../open-sse/services/geminiThoughtSignatureStore.ts");
type UnknownRecord = Record<string, unknown>;
type GeminiRequestWithConfig = { generationConfig: UnknownRecord };
type GeminiRequestWithSystem = { systemInstruction: { role?: unknown; parts?: unknown } };
test.beforeEach(() => {
clearGeminiThoughtSignatures();
@@ -76,6 +78,28 @@ test("OpenAI -> Gemini helper converts text, images and files into Gemini parts"
assert.deepEqual(convertOpenAIContentToParts("raw text"), [{ text: "raw text" }]);
});
test("OpenAI -> Gemini does not inject default maxOutputTokens for unknown caps", () => {
const withoutRequestLimit = openaiToGeminiRequest(
"gemini-2.5-pro",
{ messages: [{ role: "user", content: "Hello" }] },
false
);
assert.equal(
(withoutRequestLimit as GeminiRequestWithConfig).generationConfig.maxOutputTokens,
undefined
);
const withRequestLimit = openaiToGeminiRequest(
"gemini-2.5-pro",
{ messages: [{ role: "user", content: "Hello" }], max_tokens: 32000 },
false
);
assert.equal(
(withRequestLimit as GeminiRequestWithConfig).generationConfig.maxOutputTokens,
32000
);
});
test("OpenAI -> Gemini helper cleans complex JSON Schema structures for Gemini compatibility", () => {
const cleaned = cleanJSONSchemaForAntigravity({
type: "object",
@@ -237,11 +261,9 @@ test("OpenAI -> Gemini request maps messages, merged system instructions, tools
false
);
assert.equal((result as any).systemInstruction.role, "system");
assert.deepEqual((result as any).systemInstruction.parts, [
{ text: "Rule A" },
{ text: "Rule B" },
]);
const systemInstruction = (result as GeminiRequestWithSystem).systemInstruction;
assert.equal(systemInstruction.role, "system");
assert.deepEqual(systemInstruction.parts, [{ text: "Rule A" }, { text: "Rule B" }]);
assert.equal(result.contents[0].role, "user");
assert.deepEqual(result.contents[0].parts, [
{ text: "What is the weather?" },
@@ -270,12 +292,13 @@ test("OpenAI -> Gemini request maps messages, merged system instructions, tools
response: { result: { temp: 20 } },
});
assert.equal((result as any).generationConfig.maxOutputTokens, 2222);
assert.equal((result as any).generationConfig.temperature, 0.3);
assert.equal((result as any).generationConfig.topP, 0.9);
assert.deepEqual((result as any).generationConfig.stopSequences, ["DONE"]);
assert.equal((result as any).generationConfig.responseMimeType, "application/json");
const responseSchema = (result as any).generationConfig.responseSchema as {
const generationConfig = (result as GeminiRequestWithConfig).generationConfig;
assert.equal(generationConfig.maxOutputTokens, 2222);
assert.equal(generationConfig.temperature, 0.3);
assert.equal(generationConfig.topP, 0.9);
assert.deepEqual(generationConfig.stopSequences, ["DONE"]);
assert.equal(generationConfig.responseMimeType, "application/json");
const responseSchema = generationConfig.responseSchema as {
properties: { answer: { type: string; enum?: string[] } };
};
assert.equal(responseSchema.properties.answer.type, "string");
@@ -690,17 +713,19 @@ test("OpenAI -> Antigravity Gemini omits signature-less historical tool calls an
"signature-less historical call MUST be emitted as native functionCall (bypass applied)"
);
assert.equal(
modelTurn?.parts.some((part) => part.thoughtSignature === "skip_thought_signature_validator") ?? false,
modelTurn?.parts.some((part) => part.thoughtSignature === "skip_thought_signature_validator") ??
false,
true,
"the bypass sentinel must be injected as thoughtSignature"
);
const toolTurn = result.request.contents.find(
(content) =>
content.role === "user" &&
content.parts.some((part) => part.functionResponse)
(content) => content.role === "user" && content.parts.some((part) => part.functionResponse)
);
assert.ok(
toolTurn,
"expected signature-less tool response to be preserved as native functionResponse (bypass applied)"
);
assert.ok(toolTurn, "expected signature-less tool response to be preserved as native functionResponse (bypass applied)");
assert.equal(
toolTurn.parts.some((part) => part.functionResponse),
true,