diff --git a/src/agents/gigachat-stream.tool-calls.test.ts b/src/agents/gigachat-stream.tool-calls.test.ts index 745c2cbc59a..741c5362bea 100644 --- a/src/agents/gigachat-stream.tool-calls.test.ts +++ b/src/agents/gigachat-stream.tool-calls.test.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const updateToken = vi.fn(async () => {}); const request = vi.fn(); +const clientConfigs: Array> = []; vi.mock("gigachat", () => { class MockGigaChat { @@ -10,6 +11,10 @@ vi.mock("gigachat", () => { _accessToken = { access_token: "test-token" }; updateToken = updateToken; + + constructor(config: Record) { + clientConfigs.push(config); + } } return { GigaChat: MockGigaChat }; @@ -24,6 +29,7 @@ function createSseStream(lines: string[]): Readable { describe("createGigachatStreamFn tool calling", () => { beforeEach(() => { vi.clearAllMocks(); + clientConfigs.length = 0; }); it("round-trips sanitized tool names for streamed function calls", async () => { @@ -273,4 +279,37 @@ describe("createGigachatStreamFn tool calling", () => { expect(updateToken).not.toHaveBeenCalled(); expect(request).not.toHaveBeenCalled(); }); + + it("honors oauth auth mode even when credentials contain a colon", async () => { + request.mockResolvedValueOnce({ + status: 200, + data: createSseStream(['data: {"choices":[{"delta":{"content":"done"}}]}', "data: [DONE]"]), + }); + + const streamFn = createGigachatStreamFn({ + baseUrl: "https://gigachat.devices.sberbank.ru/api/v1", + authMode: "oauth", + scope: "GIGACHAT_API_PERS", + }); + + const stream = streamFn( + { api: "gigachat", provider: "gigachat", id: "GigaChat-2-Max" } as never, + { + messages: [], + tools: [], + } as never, + { apiKey: "oauth:credential:with:colon" } as never, + ); + + const event = await stream.result(); + + expect(event.content).toEqual([{ type: "text", text: "done" }]); + expect(clientConfigs).toHaveLength(1); + expect(clientConfigs[0]).toMatchObject({ + credentials: "oauth:credential:with:colon", + scope: "GIGACHAT_API_PERS", + }); + expect(clientConfigs[0]?.user).toBeUndefined(); + expect(clientConfigs[0]?.password).toBeUndefined(); + }); }); diff --git a/src/agents/gigachat-stream.ts b/src/agents/gigachat-stream.ts index a192700daad..03ca8fba97c 100644 --- a/src/agents/gigachat-stream.ts +++ b/src/agents/gigachat-stream.ts @@ -586,7 +586,6 @@ export function createGigachatStreamFn(opts: GigachatStreamOptions): StreamFn { // Build auth config const apiKey = options?.apiKey ?? ""; - const isUserPassCredentials = apiKey.includes(":"); const clientConfig: GigaChatClientConfig = { baseUrl: effectiveBaseUrl, @@ -601,7 +600,7 @@ export function createGigachatStreamFn(opts: GigachatStreamOptions): StreamFn { } // Set credentials based on auth mode - if (isUserPassCredentials) { + if (opts.authMode === "basic") { const { user, password } = parseGigachatBasicCredentials(apiKey); clientConfig.user = user; clientConfig.password = password; diff --git a/src/commands/onboard-non-interactive.provider-auth.test.ts b/src/commands/onboard-non-interactive.provider-auth.test.ts index 29e8b74f227..6b200fb1651 100644 --- a/src/commands/onboard-non-interactive.provider-auth.test.ts +++ b/src/commands/onboard-non-interactive.provider-auth.test.ts @@ -638,6 +638,23 @@ describe("onboard (non-interactive): provider auth", () => { }); }); + it.each(["gigachat-basic", "gigachat-business", "gigachat-personal"] as const)( + 'rejects "%s" auth choice in non-interactive mode', + async (authChoice) => { + await withOnboardEnv( + `openclaw-onboard-${authChoice}-non-interactive-`, + async ({ runtime }) => { + await expect( + runNonInteractiveOnboardingWithDefaults(runtime, { + authChoice, + skipSkills: true, + }), + ).rejects.toThrow(`Auth choice "${authChoice}" requires interactive mode.`); + }, + ); + }, + ); + it("stores LiteLLM API key and sets default model", async () => { await withOnboardEnv("openclaw-onboard-litellm-", async (env) => { const cfg = await runOnboardingAndReadConfig(env, { diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index c52be44afda..d9f7338af83 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -189,6 +189,21 @@ export async function applyNonInteractiveAuthChoice(params: { return simpleApiKeyChoice; } + if ( + authChoice === "gigachat-basic" || + authChoice === "gigachat-business" || + authChoice === "gigachat-personal" + ) { + runtime.error( + [ + `Auth choice "${authChoice}" requires interactive mode.`, + 'Use "--gigachat-api-key" for non-interactive personal OAuth onboarding, or run interactive onboarding for business/basic GigaChat setup.', + ].join("\n"), + ); + runtime.exit(1); + return null; + } + if (authChoice === "cloudflare-ai-gateway-api-key") { const accountId = opts.cloudflareAiGatewayAccountId?.trim() ?? ""; const gatewayId = opts.cloudflareAiGatewayGatewayId?.trim() ?? "";