From 302201526e54b0e9ecd7120655c703a717f88b7f Mon Sep 17 00:00:00 2001 From: Alexander Davydov Date: Fri, 20 Mar 2026 11:09:17 +0300 Subject: [PATCH] GigaChat: reuse OAuth profiles and keep prompts exact --- src/agents/gigachat-stream.tool-calls.test.ts | 51 +++++++++++++++++ src/agents/gigachat-stream.ts | 5 -- .../auth-choice.api-key-providers.test.ts | 57 ++++++++++++++----- .../local/auth-choice.api-key-providers.ts | 6 +- 4 files changed, 97 insertions(+), 22 deletions(-) diff --git a/src/agents/gigachat-stream.tool-calls.test.ts b/src/agents/gigachat-stream.tool-calls.test.ts index f3510f29850..832de40cb34 100644 --- a/src/agents/gigachat-stream.tool-calls.test.ts +++ b/src/agents/gigachat-stream.tool-calls.test.ts @@ -502,6 +502,57 @@ describe("createGigachatStreamFn tool calling", () => { expect(event.content).toEqual([{ type: "text", text: "done" }]); }); + it("preserves exact Unicode punctuation in system, user, and assistant history text", async () => { + request.mockResolvedValueOnce({ + status: 200, + data: createSseStream(['data: {"choices":[{"delta":{"content":"done"}}]}', "data: [DONE]"]), + }); + + const systemPrompt = "Keep “curly quotes”, em dashes —, NBSP\u00A0gaps, and ellipses…"; + const userText = "User asked to preserve “exact” punctuation — including\u00A0spacing…"; + const assistantText = "Assistant replied with “quoted” text — unchanged\u00A0too…"; + + const streamFn = createGigachatStreamFn({ + baseUrl: "https://gigachat.devices.sberbank.ru/api/v1", + authMode: "oauth", + }); + + const stream = await streamFn( + { api: "gigachat", provider: "gigachat", id: "GigaChat-2-Max" } as never, + { + systemPrompt, + messages: [ + { + role: "user", + content: userText, + }, + { + role: "assistant", + content: [{ type: "text", text: assistantText }], + }, + ], + tools: [], + } as never, + { apiKey: "token" } as never, + ); + + await expect(stream.result()).resolves.toMatchObject({ + content: [{ type: "text", text: "done" }], + }); + + expect(request).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + messages: [ + expect.objectContaining({ role: "system", content: systemPrompt }), + expect.objectContaining({ role: "user", content: userText }), + expect.objectContaining({ role: "assistant", content: assistantText }), + ], + }), + }), + ); + }); + it("preserves all historical tool calls from a single assistant turn", async () => { request.mockResolvedValueOnce({ status: 200, diff --git a/src/agents/gigachat-stream.ts b/src/agents/gigachat-stream.ts index 2f98de6c535..c260fd8c4d4 100644 --- a/src/agents/gigachat-stream.ts +++ b/src/agents/gigachat-stream.ts @@ -293,11 +293,6 @@ function sanitizeContent(content: string | null | undefined): string { content // eslint-disable-next-line no-control-regex .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "") - .replace(/[\u201C\u201D]/g, '"') - .replace(/[\u2018\u2019]/g, "'") - .replace(/[\u2013\u2014]/g, "-") - .replace(/\u00A0/g, " ") - .replace(/\u2026/g, "...") ); } diff --git a/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.test.ts b/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.test.ts index 28e3aef178f..dfc8aa9d40f 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.test.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.test.ts @@ -43,14 +43,17 @@ describe("applySimpleNonInteractiveApiKeyChoice", () => { applyLitellmConfig.mockImplementation((cfg: OpenClawConfig) => cfg); }); - it("disables profile fallback for GigaChat personal OAuth onboarding", async () => { + it("allows stored OAuth profile fallback for GigaChat personal OAuth onboarding", async () => { const agentDir = "/tmp/openclaw-agents/work/agent"; const nextConfig = { agents: { defaults: {} } } as OpenClawConfig; const resolveApiKey = vi.fn(async () => ({ key: "gigachat-oauth-credentials", - source: "env" as const, + source: "profile" as const, })); const maybeSetResolvedApiKey = vi.fn(async (resolved, setter) => { + if (resolved.source === "profile") { + return true; + } await setter(resolved.key); return true; }); @@ -73,20 +76,11 @@ describe("applySimpleNonInteractiveApiKeyChoice", () => { flagName: "--gigachat-api-key", envVar: "GIGACHAT_CREDENTIALS", agentDir, - allowProfile: false, + allowProfile: true, }), ); expect(maybeSetResolvedApiKey).toHaveBeenCalledOnce(); - expect(setGigachatApiKey).toHaveBeenCalledWith( - "gigachat-oauth-credentials", - agentDir, - undefined, - { - authMode: "oauth", - insecureTls: "false", - scope: "GIGACHAT_API_PERS", - }, - ); + expect(setGigachatApiKey).not.toHaveBeenCalled(); }); it("accepts the generic --token input for GigaChat non-interactive OAuth", async () => { @@ -120,7 +114,7 @@ describe("applySimpleNonInteractiveApiKeyChoice", () => { flagName: "--gigachat-api-key", envVar: "GIGACHAT_CREDENTIALS", agentDir, - allowProfile: false, + allowProfile: true, }), ); expect(setGigachatApiKey).toHaveBeenCalledWith( @@ -170,6 +164,41 @@ describe("applySimpleNonInteractiveApiKeyChoice", () => { expect(runtime.exit).toHaveBeenCalledWith(1); }); + it("rejects Basic-shaped stored profiles in the OAuth onboarding path", async () => { + const agentDir = "/tmp/openclaw-agents/work/agent"; + const nextConfig = { agents: { defaults: {} } } as OpenClawConfig; + const runtime: RuntimeEnv = { + error: vi.fn(), + exit: vi.fn(), + log: vi.fn(), + }; + const resolveApiKey = vi.fn(async () => ({ + key: "basic-user:basic-pass", + source: "profile" as const, + })); + const maybeSetResolvedApiKey = vi.fn(); + + const result = await applySimpleNonInteractiveApiKeyChoice({ + authChoice: "gigachat-api-key", + nextConfig, + baseConfig: nextConfig, + opts: {} as never, + runtime, + agentDir, + apiKeyStorageOptions: undefined, + resolveApiKey, + maybeSetResolvedApiKey, + }); + + expect(result).toBeNull(); + expect(maybeSetResolvedApiKey).not.toHaveBeenCalled(); + expect(setGigachatApiKey).not.toHaveBeenCalled(); + expect(runtime.error).toHaveBeenCalledWith( + expect.stringContaining("Basic user:password credentials"), + ); + expect(runtime.exit).toHaveBeenCalledWith(1); + }); + it("accepts OAuth credentials keys that contain colons", async () => { const agentDir = "/tmp/openclaw-agents/work/agent"; const nextConfig = { agents: { defaults: {} } } as OpenClawConfig; diff --git a/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.ts b/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.ts index 4d7b1d0e568..8fcaf635cdb 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.ts @@ -59,9 +59,9 @@ async function applyGigachatNonInteractiveApiKeyChoice(params: { envVar: "GIGACHAT_CREDENTIALS", runtime: params.runtime, agentDir: params.agentDir, - // Personal OAuth onboarding must not silently reuse an existing Basic - // username:password profile and then rewrite the provider to OAuth config. - allowProfile: false, + // Allow existing OAuth profiles to be reused, but reject Basic-shaped + // credentials below before any OAuth metadata/config rewrite happens. + allowProfile: true, }); if (!resolved) { return null;