merge PR #5655 via eval/pr-5655: fix(ai): redact sensitive credentials from outbound messages
This commit is contained in:
@@ -112,7 +112,7 @@ import {
|
||||
promoteResponsesToolUseStopReason,
|
||||
type SequentialCutoffSummaryState,
|
||||
} from "./openai-shared";
|
||||
import { transformMessages } from "./transform-messages";
|
||||
import { redactSensitiveInObject, transformMessages } from "./transform-messages";
|
||||
|
||||
export interface OpenAICodexResponsesOptions extends StreamOptions {
|
||||
reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
||||
@@ -3954,13 +3954,14 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
||||
| Array<ResponseInput[number]>
|
||||
| undefined;
|
||||
if (historyItems) {
|
||||
for (const item of historyItems) {
|
||||
const redactedHistoryItems = redactSensitiveInObject(historyItems).result as Array<ResponseInput[number]>;
|
||||
for (const item of redactedHistoryItems) {
|
||||
const maybe = item as { type?: string; call_id?: string };
|
||||
if (maybe.type === "custom_tool_call" && typeof maybe.call_id === "string") {
|
||||
customCallIds.add(maybe.call_id);
|
||||
}
|
||||
}
|
||||
messages.push(...historyItems);
|
||||
messages.push(...redactedHistoryItems);
|
||||
msgIndex += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -594,7 +594,11 @@ const streamOpenAIResponsesOnce = (
|
||||
error instanceof Error &&
|
||||
/previous[ _]?response/i.test(error.message) &&
|
||||
/zero[ _-]?data[ _-]?retention/i.test(error.message);
|
||||
if (!zdrRejection && !isOpenAIResponsesStalePreviousResponseError(error)) {
|
||||
const isPromptBlocked =
|
||||
error instanceof Error &&
|
||||
((error as { code?: string }).code === "invalid_prompt" ||
|
||||
/invalid_prompt|Request blocked/i.test(error.message));
|
||||
if (!zdrRejection && !isPromptBlocked && !isOpenAIResponsesStalePreviousResponseError(error)) {
|
||||
throw error;
|
||||
}
|
||||
// Server rejected the chain baseline: reset, count the failure (or
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { renderDemotedThinking } from "../dialect/demotion";
|
||||
import type { Api, AssistantMessage, Message, Model, ToolCall, ToolResultMessage, UserMessage } from "../types";
|
||||
import type {
|
||||
Api,
|
||||
AssistantMessage,
|
||||
DeveloperMessage,
|
||||
Message,
|
||||
Model,
|
||||
ToolCall,
|
||||
ToolResultMessage,
|
||||
UserMessage,
|
||||
} from "../types";
|
||||
import { isDemotedThinking, kDemotedThinking } from "../utils/block-symbols";
|
||||
|
||||
const enum ToolCallStatus {
|
||||
@@ -286,6 +295,156 @@ function normalizeAnthropicTargetToolCallId<TApi extends Api>(
|
||||
* - Preserves tool call structure (unlike converting to text summaries)
|
||||
* - Injects synthetic "aborted" tool results
|
||||
*/
|
||||
const SENSITIVE_TOKEN_RE =
|
||||
/(?<![a-zA-Z0-9_*-])(gh[opusr]_[a-zA-Z0-9_*]{36,}|github_pat_[a-zA-Z0-9_*]{36,}|glpat-[a-zA-Z0-9_*-]{20,}|sk-proj-[a-zA-Z0-9_*-]{36,}|sk-ant-[a-zA-Z0-9_*-]{36,}|sk-[a-zA-Z0-9_*-]{48,})(?![a-zA-Z0-9_*-])/gi;
|
||||
|
||||
function hasPlausibleCredentialEntropy(token: string): boolean {
|
||||
const lower = token.toLowerCase();
|
||||
const prefixLength = lower.startsWith("github_pat_")
|
||||
? "github_pat_".length
|
||||
: lower.startsWith("glpat-")
|
||||
? "glpat-".length
|
||||
: lower.startsWith("sk-proj-")
|
||||
? "sk-proj-".length
|
||||
: lower.startsWith("sk-ant-")
|
||||
? "sk-ant-".length
|
||||
: lower.startsWith("gh")
|
||||
? 4
|
||||
: 3;
|
||||
const secret = token.slice(prefixLength);
|
||||
if (/^\*+$/.test(secret)) return true;
|
||||
return [/[a-z]/, /[A-Z]/, /\d/, /[_-]/].filter(pattern => pattern.test(secret)).length >= 2;
|
||||
}
|
||||
|
||||
export function redactSensitiveCredentials(text: string): string {
|
||||
return text.replace(SENSITIVE_TOKEN_RE, match => {
|
||||
if (!hasPlausibleCredentialEntropy(match)) return match;
|
||||
const lower = match.toLowerCase();
|
||||
if (lower.startsWith("gh")) {
|
||||
return "[github_token_redacted]";
|
||||
}
|
||||
if (lower.startsWith("gl")) {
|
||||
return "[gitlab_token_redacted]";
|
||||
}
|
||||
if (lower.startsWith("sk-ant-")) {
|
||||
return "[anthropic_token_redacted]";
|
||||
}
|
||||
if (lower.startsWith("sk")) {
|
||||
return "[openai_token_redacted]";
|
||||
}
|
||||
return "[token_redacted]";
|
||||
});
|
||||
}
|
||||
|
||||
export function redactSensitiveInObject(val: unknown): { result: unknown; changed: boolean } {
|
||||
if (typeof val === "string") {
|
||||
const redacted = redactSensitiveCredentials(val);
|
||||
return { result: redacted, changed: redacted !== val };
|
||||
}
|
||||
if (Array.isArray(val)) {
|
||||
let changed = false;
|
||||
const result = val.map(item => {
|
||||
const res = redactSensitiveInObject(item);
|
||||
if (res.changed) changed = true;
|
||||
return res.result;
|
||||
});
|
||||
return { result, changed };
|
||||
}
|
||||
if (val !== null && typeof val === "object") {
|
||||
let changed = false;
|
||||
const res: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(val)) {
|
||||
const sub = redactSensitiveInObject(v);
|
||||
if (sub.changed) changed = true;
|
||||
res[k] = sub.result;
|
||||
}
|
||||
return { result: res, changed };
|
||||
}
|
||||
return { result: val, changed: false };
|
||||
}
|
||||
|
||||
function redactSensitiveCredentialsInMessages(messages: Message[]): Message[] {
|
||||
return messages.map((msg): Message => {
|
||||
if (msg.role === "user" || msg.role === "developer") {
|
||||
const userMsg = msg as UserMessage | DeveloperMessage;
|
||||
if (typeof userMsg.content === "string") {
|
||||
const redacted = redactSensitiveCredentials(userMsg.content);
|
||||
if (redacted === userMsg.content) return msg;
|
||||
return { ...userMsg, content: redacted } as Message;
|
||||
}
|
||||
const contentArray = userMsg.content;
|
||||
let changed = false;
|
||||
const content = contentArray.map((block): UserMessage["content"][number] => {
|
||||
if (block.type === "text") {
|
||||
const redacted = redactSensitiveCredentials(block.text);
|
||||
if (redacted !== block.text) {
|
||||
changed = true;
|
||||
return { ...block, text: redacted };
|
||||
}
|
||||
}
|
||||
return block;
|
||||
});
|
||||
return (changed ? { ...userMsg, content } : userMsg) as Message;
|
||||
}
|
||||
|
||||
if (msg.role === "toolResult") {
|
||||
const toolResultMsg = msg as ToolResultMessage;
|
||||
let changed = false;
|
||||
const content = toolResultMsg.content.map((block): ToolResultMessage["content"][number] => {
|
||||
if (block.type === "text") {
|
||||
const redacted = redactSensitiveCredentials(block.text);
|
||||
if (redacted !== block.text) {
|
||||
changed = true;
|
||||
return { ...block, text: redacted };
|
||||
}
|
||||
}
|
||||
return block;
|
||||
});
|
||||
return (changed ? { ...toolResultMsg, content } : toolResultMsg) as Message;
|
||||
}
|
||||
|
||||
if (msg.role === "assistant") {
|
||||
const assistantMsg = msg as AssistantMessage;
|
||||
let changed = false;
|
||||
const content = assistantMsg.content.map((block): AssistantMessage["content"][number] => {
|
||||
if (block.type === "text") {
|
||||
const redacted = redactSensitiveCredentials(block.text);
|
||||
if (redacted !== block.text) {
|
||||
changed = true;
|
||||
return { ...block, text: redacted };
|
||||
}
|
||||
} else if (block.type === "thinking") {
|
||||
const redacted = redactSensitiveCredentials(block.thinking);
|
||||
if (redacted !== block.thinking) {
|
||||
changed = true;
|
||||
return { ...block, thinking: redacted, thinkingSignature: undefined };
|
||||
}
|
||||
} else if (block.type === "toolCall") {
|
||||
if (block.arguments) {
|
||||
const { result: redactedArgs, changed: argsChanged } = redactSensitiveInObject(block.arguments);
|
||||
if (argsChanged) {
|
||||
changed = true;
|
||||
const castArgs =
|
||||
redactedArgs && typeof redactedArgs === "object" && !Array.isArray(redactedArgs)
|
||||
? (redactedArgs as Record<string, unknown>)
|
||||
: undefined;
|
||||
return {
|
||||
...block,
|
||||
arguments: castArgs,
|
||||
thoughtSignature: undefined,
|
||||
} as AssistantMessage["content"][number];
|
||||
}
|
||||
}
|
||||
}
|
||||
return block;
|
||||
});
|
||||
return (changed ? { ...assistantMsg, content } : assistantMsg) as Message;
|
||||
}
|
||||
|
||||
return msg;
|
||||
});
|
||||
}
|
||||
|
||||
export function transformMessages<TApi extends Api>(
|
||||
messages: Message[],
|
||||
model: Model<TApi>,
|
||||
@@ -294,6 +453,10 @@ export function transformMessages<TApi extends Api>(
|
||||
duplicateToolCallIdSuffixPrefix = "_dup",
|
||||
targetCompat: Model<TApi>["compat"] = model.compat,
|
||||
): Message[] {
|
||||
// Redact sensitive credential-like patterns from all outbound messages
|
||||
// to prevent security block errors from LLM providers (e.g. invalid_prompt).
|
||||
messages = redactSensitiveCredentialsInMessages(messages);
|
||||
|
||||
// Drop assistant `toolCall` blocks with empty/whitespace `id` or `name`
|
||||
// (and their matched `toolResult` messages) before anything else looks at
|
||||
// the history. Replays of these would 400 every provider — see
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { $env } from "@oh-my-pi/pi-utils";
|
||||
import type { ResponseInput, ResponseInputItem } from "./providers/openai-responses-wire";
|
||||
import { redactSensitiveCredentials } from "./providers/transform-messages";
|
||||
import type { CacheRetention, OpenAIResponsesHistoryPayload, ProviderPayload } from "./types";
|
||||
|
||||
type OpenAIResponsesReplayItem = ResponseInput[number];
|
||||
@@ -9,7 +10,9 @@ export { isRecord } from "@oh-my-pi/pi-utils";
|
||||
export function normalizeSystemPrompts(systemPrompt: readonly string[] | string | undefined | null): string[] {
|
||||
if (systemPrompt === undefined || systemPrompt === null) return [];
|
||||
const prompts = Array.isArray(systemPrompt) ? systemPrompt : typeof systemPrompt === "string" ? [systemPrompt] : [];
|
||||
return prompts.map(prompt => prompt.toWellFormed()).filter(prompt => prompt.trim().length > 0);
|
||||
return prompts
|
||||
.map(prompt => redactSensitiveCredentials(prompt.toWellFormed()))
|
||||
.filter(prompt => prompt.trim().length > 0);
|
||||
}
|
||||
|
||||
export function normalizeToolCallId(id: string): string {
|
||||
|
||||
@@ -1205,3 +1205,34 @@ describe("openai-codex concurrent reasoning summaries", () => {
|
||||
expect(text?.text).toBe("Hello");
|
||||
});
|
||||
});
|
||||
|
||||
describe("openai-codex native history redaction", () => {
|
||||
it("redacts credentials from user provider history before replaying it", () => {
|
||||
const model = createCodexModel("gpt-5.1-codex");
|
||||
const credential = "sk-ABCdef1234567890ABCdef1234567890ABCdef1234567890ABCdef123456";
|
||||
const context: Context = {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "fallback",
|
||||
timestamp: Date.now(),
|
||||
providerPayload: {
|
||||
type: "openaiResponsesHistory",
|
||||
provider: model.provider,
|
||||
items: [{ type: "message", role: "user", content: [{ type: "input_text", text: credential }] }],
|
||||
},
|
||||
} as Context["messages"][number],
|
||||
],
|
||||
};
|
||||
|
||||
const messages = convertCodexResponsesMessages(model, context);
|
||||
|
||||
expect(messages).toEqual([
|
||||
{
|
||||
type: "message",
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "[openai_token_redacted]" }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -230,6 +230,58 @@ describe("openai-responses stateful chaining", () => {
|
||||
expect(JSON.stringify(sentRequests[2]?.input)).toContain("First question");
|
||||
expect(JSON.stringify(sentRequests[2]?.input)).toContain("Second question");
|
||||
});
|
||||
it("retries a blocked invalid_prompt previous_response_id with the full transcript", async () => {
|
||||
const sentRequests: Array<Record<string, unknown>> = [];
|
||||
const fetchMock = vi.fn(async (_input: string | URL | Request, init?: RequestInit) => {
|
||||
const request = JSON.parse(String(init?.body)) as Record<string, unknown>;
|
||||
sentRequests.push(request);
|
||||
if (typeof request.previous_response_id === "string") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: "Request blocked.",
|
||||
type: "invalid_request_error",
|
||||
code: "invalid_prompt",
|
||||
},
|
||||
}),
|
||||
{ status: 400, headers: { "content-type": "application/json" } },
|
||||
);
|
||||
}
|
||||
return createStatefulSse(`Answer ${sentRequests.length}`, `resp_${sentRequests.length}`);
|
||||
}) as FetchImpl;
|
||||
const providerSessionState = new Map<string, ProviderSessionState>();
|
||||
const options = {
|
||||
apiKey: "test-key",
|
||||
sessionId: "stateful-blocked-session",
|
||||
providerSessionState,
|
||||
statefulResponses: true,
|
||||
reasoning: "low" as const,
|
||||
fetch: fetchMock,
|
||||
};
|
||||
|
||||
const firstUser = { role: "user" as const, content: "First question", timestamp: 1000 };
|
||||
const firstResponse = await streamOpenAIResponses(
|
||||
model,
|
||||
{ systemPrompt, messages: [firstUser] },
|
||||
options,
|
||||
).result();
|
||||
const secondResponse = await streamOpenAIResponses(
|
||||
model,
|
||||
{
|
||||
systemPrompt,
|
||||
messages: [firstUser, firstResponse, { role: "user", content: "Second question", timestamp: 1001 }],
|
||||
},
|
||||
options,
|
||||
).result();
|
||||
|
||||
expect(secondResponse.stopReason).toBe("stop");
|
||||
expect(JSON.stringify(secondResponse.content)).toContain("Answer 3");
|
||||
expect(sentRequests).toHaveLength(3);
|
||||
expect(sentRequests[1]?.previous_response_id).toBe("resp_1");
|
||||
expect(sentRequests[2]?.previous_response_id).toBeUndefined();
|
||||
expect(JSON.stringify(sentRequests[2]?.input)).toContain("First question");
|
||||
expect(JSON.stringify(sentRequests[2]?.input)).toContain("Second question");
|
||||
});
|
||||
|
||||
it("disables chaining for the session after repeated stale failures and stops forcing store", async () => {
|
||||
const sentRequests: Array<Record<string, unknown>> = [];
|
||||
|
||||
@@ -86,6 +86,16 @@ describe("openai-responses system prompt routing", () => {
|
||||
expect(input.every(m => m.role !== "system")).toBe(true);
|
||||
});
|
||||
|
||||
it("redacts sensitive credentials in instructions", async () => {
|
||||
const context: Context = {
|
||||
systemPrompt: ["Token: gho_************************************"],
|
||||
messages: [{ role: "user", content: "hi", timestamp: Date.now() }],
|
||||
};
|
||||
const body = await captureRequestBody(gpt4oMiniModel, context);
|
||||
|
||||
expect(body.instructions).toBe("Token: [github_token_redacted]");
|
||||
});
|
||||
|
||||
it("omits instructions field when there is no system prompt", async () => {
|
||||
const context: Context = {
|
||||
systemPrompt: undefined,
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { transformMessages } from "@oh-my-pi/pi-ai/providers/transform-messages";
|
||||
import type { AssistantMessage, Message, Model, ToolCall, ToolResultMessage } from "@oh-my-pi/pi-ai/types";
|
||||
import { buildModel } from "@oh-my-pi/pi-catalog/build";
|
||||
|
||||
function makeModel(): Model<"openai-responses"> {
|
||||
return buildModel({
|
||||
api: "openai-responses",
|
||||
name: "GPT Test",
|
||||
id: "gpt-test",
|
||||
provider: "openai",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
contextWindow: 8192,
|
||||
maxTokens: 2048,
|
||||
input: ["text"],
|
||||
reasoning: false,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
});
|
||||
}
|
||||
|
||||
describe("transformMessages redact sensitive credentials", () => {
|
||||
it("redacts already-masked and real tokens from outbound messages", () => {
|
||||
const messages: Message[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Token: gho_************************************",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "I found this key: sk-proj-************************************",
|
||||
},
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call_x",
|
||||
name: "bash",
|
||||
arguments: {
|
||||
command: "echo gho_************************************",
|
||||
},
|
||||
},
|
||||
],
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
model: "gpt-test",
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "toolUse",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "call_x",
|
||||
toolName: "bash",
|
||||
content: [{ type: "text", text: "Token is ghp_************************************ inside output" }],
|
||||
isError: false,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
];
|
||||
|
||||
const transformed = transformMessages(messages, makeModel());
|
||||
|
||||
// 1. Verify user message is redacted
|
||||
const userMsg = transformed[0];
|
||||
expect(userMsg.role).toBe("user");
|
||||
expect(userMsg.content).toBe("Token: [github_token_redacted]");
|
||||
|
||||
// 2. Verify assistant message text and toolCall arguments are redacted
|
||||
const assistantMsg = transformed[1];
|
||||
expect(assistantMsg.role).toBe("assistant");
|
||||
const castAssistantMsg = assistantMsg as AssistantMessage;
|
||||
const assistantContent = castAssistantMsg.content;
|
||||
const textBlock = assistantContent[0];
|
||||
expect(textBlock.type).toBe("text");
|
||||
if (textBlock.type === "text") {
|
||||
expect(textBlock.text).toBe("I found this key: [openai_token_redacted]");
|
||||
}
|
||||
|
||||
const toolCallBlock = assistantContent[1];
|
||||
expect(toolCallBlock.type).toBe("toolCall");
|
||||
|
||||
// 3. Verify toolResult message is redacted
|
||||
const resultMsg = transformed[2];
|
||||
expect(resultMsg.role).toBe("toolResult");
|
||||
const toolResultMsg = resultMsg as ToolResultMessage;
|
||||
const toolResultBlock = toolResultMsg.content[0];
|
||||
expect(toolResultBlock.type).toBe("text");
|
||||
if (toolResultBlock.type === "text") {
|
||||
expect(toolResultBlock.text).toBe("Token is [github_token_redacted] inside output");
|
||||
}
|
||||
if (toolCallBlock.type === "toolCall") {
|
||||
const toolCall = toolCallBlock as ToolCall;
|
||||
const commandArg = toolCall.arguments?.command;
|
||||
expect(commandArg).toBe("echo [github_token_redacted]");
|
||||
}
|
||||
});
|
||||
|
||||
it("drops an Anthropic thinking signature when redacting its signed content", () => {
|
||||
const model = buildModel({
|
||||
api: "anthropic-messages",
|
||||
name: "Claude Test",
|
||||
id: "claude-test",
|
||||
provider: "anthropic",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
contextWindow: 8192,
|
||||
maxTokens: 2048,
|
||||
input: ["text"],
|
||||
reasoning: true,
|
||||
compat: { signingEndpoint: true },
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
});
|
||||
const messages: Message[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "thinking",
|
||||
thinking: "Use sk-ABCdef1234567890ABCdef1234567890ABCdef1234567890ABCdef123456.",
|
||||
thinkingSignature: "signed-thinking-bytes",
|
||||
},
|
||||
],
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic",
|
||||
model: "claude-test",
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "stop",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
];
|
||||
|
||||
const transformed = transformMessages(messages, model);
|
||||
|
||||
expect(transformed[0]).toMatchObject({ role: "assistant", content: [] });
|
||||
});
|
||||
|
||||
it("drops a tool thought signature after redacting its arguments", () => {
|
||||
const messages: Message[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call_signed",
|
||||
name: "run",
|
||||
arguments: { token: "sk-ABCdef1234567890ABCdef1234567890ABCdef1234567890ABCdef123456" },
|
||||
thoughtSignature: "signed-tool-arguments",
|
||||
},
|
||||
],
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
model: "gpt-test",
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "toolUse",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
];
|
||||
|
||||
const transformed = transformMessages(messages, makeModel());
|
||||
const block = (transformed[0] as AssistantMessage).content[0];
|
||||
|
||||
expect(block).toMatchObject({
|
||||
type: "toolCall",
|
||||
arguments: { token: "[openai_token_redacted]" },
|
||||
});
|
||||
if (block.type === "toolCall") {
|
||||
expect(block.thoughtSignature).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves credential-shaped prose that is not a plausible live token", () => {
|
||||
const lookalike = "sk-abcdefghijklmnopqrstuvwxyz";
|
||||
const transformed = transformMessages(
|
||||
[{ role: "user", content: `The example key is ${lookalike}.`, timestamp: Date.now() }],
|
||||
makeModel(),
|
||||
);
|
||||
|
||||
expect(transformed[0]).toMatchObject({ role: "user", content: `The example key is ${lookalike}.` });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user