improve(gateway): compose live session stress probes (#122519)

* test(gateway): compose live session stress probes

Amp-Thread-ID: https://ampcode.com/threads/T-019feaaa-c7ed-769e-9f29-a3612bec72e7

* fix(ai): resume after Responses compaction checkpoints

Amp-Thread-ID: https://ampcode.com/threads/T-019feaaa-c7ed-769e-9f29-a3612bec72e7

* test(gateway): compose multi-session subagent probes

Amp-Thread-ID: https://ampcode.com/threads/T-019feaaa-c7ed-769e-9f29-a3612bec72e7

* fix(test): invalidate inlined schema transforms

Amp-Thread-ID: https://ampcode.com/threads/T-019feaaa-c7ed-769e-9f29-a3612bec72e7

* test(ai): cover empty compaction owners

Amp-Thread-ID: https://ampcode.com/threads/T-019feaaa-c7ed-769e-9f29-a3612bec72e7

---------

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Peter Steinberger
2026-08-12 06:22:08 -07:00
committed by GitHub
parent 9b90e104c5
commit 2cb9a75648
15 changed files with 187 additions and 65 deletions
+3 -3
View File
@@ -77,9 +77,9 @@ Implementation:
- Max image side is configurable via `agents.defaults.imageMaxDimensionPx` - Max image side is configurable via `agents.defaults.imageMaxDimensionPx`
(default: `1200`) (default: `1200`)
- Blank text blocks are removed while this pass walks replay content. - Blank text blocks are removed while this pass walks replay content.
Assistant turns that become empty are dropped from the replay copy; user Assistant turns that become empty are dropped unless they own opaque
and tool-result turns that become empty receive a non-empty provider replay state; user and tool-result turns that become empty receive
omitted-content placeholder. a non-empty omitted-content placeholder.
--- ---
@@ -928,19 +928,15 @@ describe("OpenAI Responses compaction replay", () => {
expect(input.map((item) => item.type)).toEqual(["compaction", "message"]); expect(input.map((item) => item.type)).toEqual(["compaction", "message"]);
}); });
it("replays when session and auth identities match", () => { it.each(responseConverters)(
const assistant = createOutput(); "$name replays an empty checkpoint owner when request identities match",
assistant.providerReplay = compactionState(model, { replayIndex: 0 }); ({ convert }) => {
const assistant = createOutput();
assistant.providerReplay = compactionState(model, { replayIndex: 0 });
const input = convertResponsesMessages( expect(convert({ messages: [assistant] }).map((item) => item.type)).toEqual(["compaction"]);
model, },
{ messages: [assistant] }, );
new Set(["openai"]),
replayIdentity,
);
expect(input.some((item) => item.type === "compaction")).toBe(true);
});
it.each(responseConverters)( it.each(responseConverters)(
"$name does not replay or prune across a different or missing request identity", "$name does not replay or prune across a different or missing request identity",
@@ -21,9 +21,18 @@ export function createStateSchemaInlinePlugin(rootDir = process.cwd()) {
const schemasByModulePath = new Map( const schemasByModulePath = new Map(
STATE_SCHEMA_MODULES.map((schema) => [path.resolve(rootDir, schema.modulePath), schema]), STATE_SCHEMA_MODULES.map((schema) => [path.resolve(rootDir, schema.modulePath), schema]),
); );
const cacheKeyForSchema = ({ id }: { id: string }) => {
const schema = schemasByModulePath.get(path.resolve(id));
return schema ? fs.readFileSync(path.resolve(rootDir, schema.schemaPath), "utf8") : undefined;
};
return { return {
name: STATE_SCHEMA_INLINE_PLUGIN_NAME, name: STATE_SCHEMA_INLINE_PLUGIN_NAME,
configureVitest(context: {
experimental_defineCacheKeyGenerator(callback: typeof cacheKeyForSchema): void;
}) {
context.experimental_defineCacheKeyGenerator(cacheKeyForSchema);
},
load(this: { addWatchFile(id: string): void }, id: string) { load(this: { addWatchFile(id: string): void }, id: string) {
const schema = schemasByModulePath.get(path.resolve(id)); const schema = schemasByModulePath.get(path.resolve(id));
if (!schema) { if (!schema) {
@@ -250,6 +250,31 @@ describe("sanitizeSessionMessagesImages", () => {
expect(out).toHaveLength(1); expect(out).toHaveLength(1);
expect(out[0]?.role).toBe("user"); expect(out[0]?.role).toBe("user");
}); });
it.each([
["full", "length"],
["images-only", "length"],
["full", "error"],
["images-only", "error"],
] as const)(
"preserves an empty provider replay owner in %s mode after %s",
async (sanitizeMode, stopReason) => {
const checkpoint = {
...makeOpenAiResponsesAssistantMessage([{ type: "text", text: "" }], stopReason),
providerReplay: {
v: 1,
type: "opaque-checkpoint",
data: "opaque-state",
provider: "openai",
api: "openai-responses",
model: "gpt-5.4",
},
} satisfies AssistantMessage;
const out = await sanitizeSessionMessagesImages([checkpoint], "test", { sanitizeMode });
expect(out).toEqual([{ ...checkpoint, content: [] }]);
},
);
it("drops empty assistant error messages", async () => { it("drops empty assistant error messages", async () => {
const input = castAgentMessages([ const input = castAgentMessages([
{ role: "user", content: "hello", timestamp: nextTimestamp() } satisfies UserMessage, { role: "user", content: "hello", timestamp: nextTimestamp() } satisfies UserMessage,
+4 -20
View File
@@ -53,8 +53,6 @@ export async function sanitizeSessionMessagesImages(
}; };
} & ImageSanitizationLimits, } & ImageSanitizationLimits,
): Promise<AgentMessage[]> { ): Promise<AgentMessage[]> {
const sanitizeMode = options?.sanitizeMode ?? "full";
const allowNonImageSanitization = sanitizeMode === "full";
const imageSanitization = { const imageSanitization = {
maxDimensionPx: options?.maxDimensionPx, maxDimensionPx: options?.maxDimensionPx,
maxBytes: options?.maxBytes, maxBytes: options?.maxBytes,
@@ -113,7 +111,7 @@ export async function sanitizeSessionMessagesImages(
imageSanitization, imageSanitization,
)) as unknown as typeof assistantMsg.content; )) as unknown as typeof assistantMsg.content;
const finalContent = dropEmptyTextBlocks(nextContent); const finalContent = dropEmptyTextBlocks(nextContent);
if (finalContent.length > 0) { if (finalContent.length > 0 || assistantMsg.providerReplay) {
out.push({ ...assistantMsg, content: finalContent }); out.push({ ...assistantMsg, content: finalContent });
} }
} else { } else {
@@ -126,28 +124,14 @@ export async function sanitizeSessionMessagesImages(
const strippedContent = options?.preserveSignatures const strippedContent = options?.preserveSignatures
? content // Keep signatures for Antigravity Claude ? content // Keep signatures for Antigravity Claude
: stripThoughtSignatures(content, options?.sanitizeThoughtSignatures); // Strip for Gemini : stripThoughtSignatures(content, options?.sanitizeThoughtSignatures); // Strip for Gemini
if (!allowNonImageSanitization) {
const nextContent = (await sanitizeContentBlocksImages(
dropEmptyTextBlocks(strippedContent) as unknown as ContentBlock[],
label,
imageSanitization,
)) as unknown as typeof assistantMsg.content;
if (nextContent.length > 0) {
out.push({ ...assistantMsg, content: nextContent });
}
continue;
}
const filteredContent = dropEmptyTextBlocks(strippedContent);
const finalContent = (await sanitizeContentBlocksImages( const finalContent = (await sanitizeContentBlocksImages(
filteredContent as unknown as ContentBlock[], dropEmptyTextBlocks(strippedContent) as unknown as ContentBlock[],
label, label,
imageSanitization, imageSanitization,
)) as unknown as typeof assistantMsg.content; )) as unknown as typeof assistantMsg.content;
if (finalContent.length === 0) { if (finalContent.length > 0 || assistantMsg.providerReplay) {
continue; out.push({ ...assistantMsg, content: finalContent });
} }
out.push({ ...assistantMsg, content: finalContent });
continue; continue;
} }
} }
@@ -134,6 +134,48 @@ describe("runEmbeddedAgent incomplete-turn safety", () => {
expectWarnMessageWith("empty response detected"); expectWarnMessageWith("empty response detected");
}); });
it("continues after an OpenAI Responses compaction-only incomplete turn", async () => {
const checkpoint = makeLastAssistant({
api: "openai-responses",
provider: "openai",
model: "gpt-5.6-luna",
stopReason: "length",
providerReplay: {
v: 1,
type: "openai-responses-compaction",
data: "opaque-checkpoint",
provider: "openai",
api: "openai-responses",
model: "gpt-5.6-luna",
},
});
mockedRunEmbeddedAttempt.mockResolvedValueOnce(
makeAttemptResult({
assistantTexts: [],
currentAttemptAssistant: checkpoint,
lastAssistant: checkpoint,
}),
);
mockedRunEmbeddedAttempt.mockResolvedValueOnce(
makeAttemptResult({
assistantTexts: ["Visible answer after compaction."],
lastAssistant: makeLastAssistant({
content: [{ type: "text", text: "Visible answer after compaction." }],
}),
}),
);
await runEmbeddedAgent(
makeRunParams("run-provider-compaction-continuation", {
provider: "openai",
model: "gpt-5.6-luna",
}),
);
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
expectWarnMessageWith("compaction interrupted visible final answer");
});
it("retries empty Anthropic-compatible stop turns even when the provider is not Kimi", async () => { it("retries empty Anthropic-compatible stop turns even when the provider is not Kimi", async () => {
mockedClassifyFailoverReason.mockReturnValue(null); mockedClassifyFailoverReason.mockReturnValue(null);
mockedResolveModelAsync.mockResolvedValue({ mockedResolveModelAsync.mockResolvedValue({
@@ -341,7 +341,8 @@ export async function resolveEmbeddedRunTerminal(input: {
if ( if (
!emptyAssistantReplyIsSilent && !emptyAssistantReplyIsSilent &&
!settledTurnFinalizationAttempted && !settledTurnFinalizationAttempted &&
input.attemptCompactionCount > 0 && (input.attemptCompactionCount > 0 ||
attempt.currentAttemptAssistant?.providerReplay?.type === "openai-responses-compaction") &&
payloadCount === 0 && payloadCount === 0 &&
!terminalInterrupted && !terminalInterrupted &&
!promptError && !promptError &&
@@ -62,19 +62,21 @@ describe("gateway codex harness live helpers", () => {
guardianProbe: false, guardianProbe: false,
imageProbe: false, imageProbe: false,
mcpProbe: false, mcpProbe: false,
multiSessionProbe: false,
resumeStress: false, resumeStress: false,
subagentProbe: true, subagentProbe: true,
}; };
expect(shouldUseCodexHarnessSubagentOnlyFastPath(base)).toBe(true); expect(shouldUseCodexHarnessSubagentOnlyFastPath(base)).toBe(true);
expect(shouldUseCodexHarnessSubagentOnlyFastPath({ ...base, resumeStress: true })).toBe(false); for (const flag of [
expect(shouldUseCodexHarnessSubagentOnlyFastPath({ ...base, compactionStress: true })).toBe( "codeModeOnly",
false, "compactionStress",
); "explicitOptOut",
expect(shouldUseCodexHarnessSubagentOnlyFastPath({ ...base, codeModeOnly: true })).toBe(false); "multiSessionProbe",
expect(shouldUseCodexHarnessSubagentOnlyFastPath({ ...base, explicitOptOut: true })).toBe( "resumeStress",
false, ] as const) {
); expect(shouldUseCodexHarnessSubagentOnlyFastPath({ ...base, [flag]: true })).toBe(false);
}
}); });
it("classifies sessions.list timeouts as retryable live Codex errors", () => { it("classifies sessions.list timeouts as retryable live Codex errors", () => {
@@ -96,6 +96,7 @@ export function shouldUseCodexHarnessSubagentOnlyFastPath(params: {
guardianProbe: boolean; guardianProbe: boolean;
imageProbe: boolean; imageProbe: boolean;
mcpProbe: boolean; mcpProbe: boolean;
multiSessionProbe: boolean;
resumeStress: boolean; resumeStress: boolean;
subagentProbe: boolean; subagentProbe: boolean;
}): boolean { }): boolean {
@@ -107,6 +108,7 @@ export function shouldUseCodexHarnessSubagentOnlyFastPath(params: {
!params.guardianProbe && !params.guardianProbe &&
!params.imageProbe && !params.imageProbe &&
!params.mcpProbe && !params.mcpProbe &&
!params.multiSessionProbe &&
!params.resumeStress && !params.resumeStress &&
!params.explicitOptOut !params.explicitOptOut
); );
@@ -148,6 +148,7 @@ const CODEX_HARNESS_SUBAGENT_ONLY = shouldUseCodexHarnessSubagentOnlyFastPath({
guardianProbe: CODEX_HARNESS_GUARDIAN_PROBE, guardianProbe: CODEX_HARNESS_GUARDIAN_PROBE,
imageProbe: CODEX_HARNESS_IMAGE_PROBE, imageProbe: CODEX_HARNESS_IMAGE_PROBE,
mcpProbe: CODEX_HARNESS_MCP_PROBE, mcpProbe: CODEX_HARNESS_MCP_PROBE,
multiSessionProbe: CODEX_HARNESS_MULTI_SESSION_PROBE,
resumeStress: CODEX_HARNESS_RESUME_STRESS, resumeStress: CODEX_HARNESS_RESUME_STRESS,
subagentProbe: CODEX_HARNESS_SUBAGENT_PROBE, subagentProbe: CODEX_HARNESS_SUBAGENT_PROBE,
}); });
@@ -2209,7 +2210,6 @@ describeLive("gateway live (Codex harness)", () => {
}, },
workspace, workspace,
}); });
break;
} }
if (CODEX_HARNESS_SUBAGENT_PROBE) { if (CODEX_HARNESS_SUBAGENT_PROBE) {
@@ -503,8 +503,12 @@ describeLive("Gateway OpenAI long-context compaction (live)", () => {
} }
} }
if (!compactionState?.latest) { if (!compactionState?.latest) {
const thresholdEvidence =
peakPromptTokens > 0
? `peak provider prompt tokens=${peakPromptTokens}, compact threshold=${profile.compactThreshold}`
: "provider prompt-token usage unavailable";
throw new Error( throw new Error(
`OpenAI emitted no first-class compaction item after ${profile.maxDenseTurns} dense turns`, `OpenAI emitted no first-class compaction item after ${profile.maxDenseTurns} dense turns; ${thresholdEvidence}`,
); );
} }
expect(compactionState.latest).toMatchObject({ expect(compactionState.latest).toMatchObject({
+10
View File
@@ -111,6 +111,12 @@ describe("tsdown config", () => {
const rootDir = process.cwd(); const rootDir = process.cwd();
const watchedPaths: string[] = []; const watchedPaths: string[] = [];
const plugin = createStateSchemaInlinePlugin(rootDir); const plugin = createStateSchemaInlinePlugin(rootDir);
let cacheKeyGenerator: ((context: { id: string }) => string | undefined) | undefined;
plugin.configureVitest({
experimental_defineCacheKeyGenerator: (generator) => {
cacheKeyGenerator = generator;
},
});
const result = plugin.load.call( const result = plugin.load.call(
{ addWatchFile: (filePath: string) => watchedPaths.push(filePath) }, { addWatchFile: (filePath: string) => watchedPaths.push(filePath) },
path.resolve(rootDir, schema.modulePath), path.resolve(rootDir, schema.modulePath),
@@ -126,6 +132,10 @@ describe("tsdown config", () => {
expect(JSON.parse(match?.[1] ?? "null")).toBe(canonicalSql); expect(JSON.parse(match?.[1] ?? "null")).toBe(canonicalSql);
expect(schema.sourceValue).toBe(canonicalSql); expect(schema.sourceValue).toBe(canonicalSql);
expect(watchedPaths).toEqual([schemaPath]); expect(watchedPaths).toEqual([schemaPath]);
expect(cacheKeyGenerator?.({ id: path.resolve(rootDir, schema.modulePath) })).toBe(
canonicalSql,
);
expect(cacheKeyGenerator?.({ id: path.resolve(rootDir, "src/index.ts") })).toBeUndefined();
}); });
it("installs schema inlining only on the unified runtime graph", () => { it("installs schema inlining only on the unified runtime graph", () => {
@@ -63,7 +63,9 @@ describe("Gateway OpenAI Responses compaction replay", () => {
}); });
try { try {
await runAgentTurn(client, "capture compaction state"); await runAgentTurn(client, "capture compaction state");
expect(modelServer.requests).toHaveLength(1); // The provider can terminate after emitting only a compaction item. The
// runner must continue from that checkpoint before completing the turn.
expect(modelServer.requests).toHaveLength(2);
const session = await client.request<{ const session = await client.request<{
sessions?: Array<{ key?: string; sessionId?: string }>; sessions?: Array<{ key?: string; sessionId?: string }>;
@@ -78,28 +80,31 @@ describe("Gateway OpenAI Responses compaction replay", () => {
sessionKey: SESSION_KEY, sessionKey: SESSION_KEY,
storePath: path.join(instance.state.agentDir("main"), "openclaw-agent.sqlite"), storePath: path.join(instance.state.agentDir("main"), "openclaw-agent.sqlite"),
}); });
const persistedReplay = manager const contextMessages = manager.buildSessionContext().messages;
.buildSessionContext() const persistedReplay = contextMessages.find(
.messages.find((message) => message.role === "assistant")?.providerReplay; (message) => message.role === "assistant",
)?.providerReplay;
expect(persistedReplay).toMatchObject({ expect(persistedReplay).toMatchObject({
v: 1,
type: "openai-responses-compaction", type: "openai-responses-compaction",
id: COMPACTION_ID, id: COMPACTION_ID,
data: COMPACTION_DATA, data: COMPACTION_DATA,
provider: "replay-proof", provider: "replay-proof",
api: "openai-responses", api: "openai-responses",
model: "replay-proof", model: "replay-proof",
baseUrlHash: expect.any(String),
sessionHash: expect.any(String), sessionHash: expect.any(String),
}); });
expect(persistedReplay).not.toHaveProperty("authProfileHash"); expect(persistedReplay).not.toHaveProperty("authProfileHash");
expectCompactionReplay(modelServer.requests[1]?.body.input ?? []);
expect(JSON.stringify(modelServer.requests[1]?.body.input)).toContain(
"Continue from the compacted transcript",
);
await runAgentTurn(client, "replay compaction state"); await runAgentTurn(client, "replay compaction state");
expect(modelServer.requests).toHaveLength(2); expect(modelServer.requests).toHaveLength(3);
const replayInput = modelServer.requests[1]?.body.input ?? []; const replayInput = modelServer.requests[2]?.body.input ?? [];
expect(replayInput).toContainEqual({ expectCompactionReplay(replayInput);
type: "compaction",
id: COMPACTION_ID,
encrypted_content: COMPACTION_DATA,
});
const compactionIndex = replayInput.findIndex( const compactionIndex = replayInput.findIndex(
(item) => (item) =>
typeof item === "object" && typeof item === "object" &&
@@ -120,7 +125,7 @@ describe("Gateway OpenAI Responses compaction replay", () => {
).toBe(true); ).toBe(true);
const encodedReplayInput = JSON.stringify(replayInput); const encodedReplayInput = JSON.stringify(replayInput);
expect(encodedReplayInput).not.toContain("capture compaction state"); expect(encodedReplayInput).not.toContain("capture compaction state");
expect(encodedReplayInput).toContain("gateway replay response 1"); expect(encodedReplayInput).toContain("gateway replay response 2");
expect(encodedReplayInput).toContain("replay compaction state"); expect(encodedReplayInput).toContain("replay compaction state");
} finally { } finally {
await disconnectGatewayClient(client); await disconnectGatewayClient(client);
@@ -188,6 +193,14 @@ async function runAgentTurn(
return runId; return runId;
} }
function expectCompactionReplay(input: unknown[]): void {
expect(input).toContainEqual({
type: "compaction",
id: COMPACTION_ID,
encrypted_content: COMPACTION_DATA,
});
}
async function startMockModelServer(): Promise<MockModelServer> { async function startMockModelServer(): Promise<MockModelServer> {
const requests: CapturedRequest[] = []; const requests: CapturedRequest[] = [];
const server = createServer((request, response) => { const server = createServer((request, response) => {
@@ -241,6 +254,28 @@ async function handleRequest(
} }
function writeModelResponse(response: ServerResponse, sequence: number): void { function writeModelResponse(response: ServerResponse, sequence: number): void {
if (sequence === 1) {
const compaction = {
type: "compaction",
id: COMPACTION_ID,
encrypted_content: COMPACTION_DATA,
};
writeSseEvents(response, [
{ type: "response.output_item.added", output_index: 0, item: compaction },
{ type: "response.output_item.done", output_index: 0, item: compaction },
{
type: "response.incomplete",
response: {
id: "resp_gateway_replay_1",
status: "incomplete",
incomplete_details: { reason: "max_output_tokens" },
output: [compaction],
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
},
},
]);
return;
}
const text = `gateway replay response ${sequence}`; const text = `gateway replay response ${sequence}`;
const message = { const message = {
type: "message", type: "message",
@@ -249,10 +284,7 @@ function writeModelResponse(response: ServerResponse, sequence: number): void {
status: "completed", status: "completed",
content: [{ type: "output_text", text, annotations: [] }], content: [{ type: "output_text", text, annotations: [] }],
}; };
const output = const output = [message];
sequence === 1
? [{ type: "compaction", id: COMPACTION_ID, encrypted_content: COMPACTION_DATA }, message]
: [message];
const events: MockSseEvent[] = output.flatMap((item, outputIndex) => [ const events: MockSseEvent[] = output.flatMap((item, outputIndex) => [
{ {
type: "response.output_item.added", type: "response.output_item.added",
@@ -270,6 +302,10 @@ function writeModelResponse(response: ServerResponse, sequence: number): void {
usage: { input_tokens: 10, output_tokens: 2, total_tokens: 12 }, usage: { input_tokens: 10, output_tokens: 2, total_tokens: 12 },
}, },
}); });
writeSseEvents(response, events);
}
function writeSseEvents(response: ServerResponse, events: MockSseEvent[]): void {
response.writeHead(200, { response.writeHead(200, {
"content-type": "text/event-stream", "content-type": "text/event-stream",
"cache-control": "no-store", "cache-control": "no-store",
@@ -118,7 +118,7 @@ describe("OpenAI long-context live settings", () => {
contextWindow: 48_000, contextWindow: 48_000,
contextTokens: 48_000, contextTokens: 48_000,
maxTokens: 8_192, maxTokens: 8_192,
compactThreshold: 32_000, compactThreshold: 1_000,
}); });
const full = resolveOpenAILongContextLiveSettings( const full = resolveOpenAILongContextLiveSettings(
{ {
+13 -2
View File
@@ -48,9 +48,12 @@ const PROFILES = {
contextWindow: 48_000, contextWindow: 48_000,
contextTokens: 48_000, contextTokens: 48_000,
maxTokens: 8_192, maxTokens: 8_192,
compactThreshold: 32_000, // Keep the reduced live probe on OpenAI's demonstrated compaction path.
// High-threshold Luna probes can cross the configured threshold without
// emitting a checkpoint, while the 1k boundary is deterministic.
compactThreshold: 1_000,
denseTurnChars: 120_000, denseTurnChars: 120_000,
maxDenseTurns: 8, maxDenseTurns: 3,
defaultToolBytes: 300_000, defaultToolBytes: 300_000,
requestTimeoutMs: 2 * 60_000, requestTimeoutMs: 2 * 60_000,
suiteTimeoutMs: 10 * 60_000, suiteTimeoutMs: 10 * 60_000,
@@ -199,6 +202,9 @@ export function buildOpenAILongContextConfig(params: {
workspace: params.workspace, workspace: params.workspace,
skipBootstrap: true, skipBootstrap: true,
thinkingDefault: "low", thinkingDefault: "low",
// This suite owns the server-compaction threshold. Embedded proactive
// compaction would consume the same history before replay can be proved.
compaction: { enabled: false },
model: { primary: profile.modelRef }, model: { primary: profile.modelRef },
models: { models: {
[profile.modelRef]: { [profile.modelRef]: {
@@ -256,6 +262,11 @@ export function assertOpenAILongContextConfig(
cfg.secrets?.providers?.default?.source, cfg.secrets?.providers?.default?.source,
"env", "env",
); );
expectConfigValue(
"agents.defaults.compaction.enabled",
cfg.agents?.defaults?.compaction?.enabled,
false,
);
expectConfigValue("models.providers.openai.models.length", provider?.models.length, 1); expectConfigValue("models.providers.openai.models.length", provider?.models.length, 1);
const model = provider?.models[0]; const model = provider?.models[0];
expectConfigValue("model.id", model?.id, profile.modelId); expectConfigValue("model.id", model?.id, profile.modelId);