GigaChat: fix review follow-ups
This commit is contained in:
parent
b3a2ceec8a
commit
9134eb7af2
97
src/agents/gigachat-stream.test.ts
Normal file
97
src/agents/gigachat-stream.test.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
cleanSchemaForGigaChat,
|
||||
ensureJsonObjectStr,
|
||||
extractGigaChatErrorMessage,
|
||||
mapToolNameFromGigaChat,
|
||||
mapToolNameToGigaChat,
|
||||
parseGigachatBasicCredentials,
|
||||
sanitizeFunctionName,
|
||||
} from "./gigachat-stream.js";
|
||||
|
||||
describe("gigachat stream helpers", () => {
|
||||
it("maps reserved tool names to and from GigaChat-safe names", () => {
|
||||
expect(mapToolNameToGigaChat("web_search")).toBe("__gpt2giga_user_search_web");
|
||||
expect(mapToolNameFromGigaChat("__gpt2giga_user_search_web")).toBe("web_search");
|
||||
});
|
||||
|
||||
it("sanitizes tool names to GigaChat-compatible identifiers", () => {
|
||||
expect(sanitizeFunctionName("search-web!tool")).toBe("search_web_tool");
|
||||
expect(sanitizeFunctionName("___")).toBe("func");
|
||||
});
|
||||
|
||||
it("parses basic auth credentials without truncating colon-containing passwords", () => {
|
||||
expect(parseGigachatBasicCredentials("user:p@ss:with:colons")).toEqual({
|
||||
user: "user",
|
||||
password: "p@ss:with:colons",
|
||||
});
|
||||
});
|
||||
|
||||
it("cleans unsupported schema features for GigaChat", () => {
|
||||
const cleaned = cleanSchemaForGigaChat({
|
||||
type: "object",
|
||||
properties: {
|
||||
filters: {
|
||||
type: "object",
|
||||
description: "Advanced filters",
|
||||
properties: {
|
||||
level: { type: "string", enum: Array.from({ length: 25 }, (_, i) => `v${i}`) },
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
type: "array",
|
||||
items: { type: "string", minLength: 1 },
|
||||
},
|
||||
count: {
|
||||
type: ["integer", "null"],
|
||||
minimum: 1,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
expect(cleaned).toEqual({
|
||||
type: "object",
|
||||
properties: {
|
||||
filters: {
|
||||
type: "string",
|
||||
description: "Advanced filters (JSON object)",
|
||||
},
|
||||
tags: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
count: {
|
||||
type: "integer",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("wraps non-object tool results as JSON objects", () => {
|
||||
expect(ensureJsonObjectStr("plain text")).toBe(JSON.stringify({ result: "plain text" }));
|
||||
expect(ensureJsonObjectStr('{"ok":true}')).toBe('{"ok":true}');
|
||||
});
|
||||
|
||||
it("extracts readable API errors from GigaChat/Axios-like responses", () => {
|
||||
const err = new Error("[object Object]") as Error & {
|
||||
response?: {
|
||||
status?: number;
|
||||
data?: unknown;
|
||||
config?: { baseURL?: string; url?: string };
|
||||
};
|
||||
};
|
||||
err.response = {
|
||||
status: 401,
|
||||
data: { message: "invalid credentials" },
|
||||
config: {
|
||||
baseURL: "https://gigachat.devices.sberbank.ru/api/v1",
|
||||
url: "/chat/completions",
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractGigaChatErrorMessage(err)).toBe(
|
||||
"GigaChat API 401 (https://gigachat.devices.sberbank.ru/api/v1/chat/completions): invalid credentials",
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -77,6 +77,41 @@ export function mapToolNameFromGigaChat(name: string): string {
|
||||
return RESERVED_NAME_GIGA_TO_CLIENT[name] ?? name;
|
||||
}
|
||||
|
||||
export function parseGigachatBasicCredentials(credentials: string): {
|
||||
user: string;
|
||||
password: string;
|
||||
} {
|
||||
const separatorIndex = credentials.indexOf(":");
|
||||
if (separatorIndex < 0) {
|
||||
return { user: credentials, password: "" };
|
||||
}
|
||||
return {
|
||||
user: credentials.slice(0, separatorIndex),
|
||||
password: credentials.slice(separatorIndex + 1),
|
||||
};
|
||||
}
|
||||
|
||||
function toGigaChatToolName(name: string): string {
|
||||
return sanitizeFunctionName(mapToolNameToGigaChat(name));
|
||||
}
|
||||
|
||||
function rememberToolNameMapping(
|
||||
forward: Map<string, string>,
|
||||
reverse: Map<string, string>,
|
||||
originalName: string,
|
||||
): string {
|
||||
const gigaName = toGigaChatToolName(originalName);
|
||||
forward.set(originalName, gigaName);
|
||||
if (!reverse.has(gigaName)) {
|
||||
reverse.set(gigaName, originalName);
|
||||
} else if (reverse.get(gigaName) !== originalName) {
|
||||
log.warn(
|
||||
`GigaChat: tool name collision after sanitization: "${originalName}" and "${reverse.get(gigaName)}" both map to "${gigaName}"`,
|
||||
);
|
||||
}
|
||||
return gigaName;
|
||||
}
|
||||
|
||||
// ── Schema cleaning ─────────────────────────────────────────────────────────
|
||||
// GigaChat doesn't support many JSON Schema features. We track modifications
|
||||
// to help debug issues with tool definitions.
|
||||
@ -421,6 +456,8 @@ export function createGigachatStreamFn(opts: GigachatStreamOptions): StreamFn {
|
||||
try {
|
||||
const disableFunctions = process.env.GIGACHAT_DISABLE_FUNCTIONS?.trim().toLowerCase();
|
||||
const functionsEnabled = disableFunctions !== "1" && disableFunctions !== "true";
|
||||
const toolNameToGiga = new Map<string, string>();
|
||||
const gigaToToolName = new Map<string, string>();
|
||||
|
||||
// Build messages for GigaChat format
|
||||
const messages: Message[] = [];
|
||||
@ -451,11 +488,16 @@ export function createGigachatStreamFn(opts: GigachatStreamOptions): StreamFn {
|
||||
const toolCall = contentParts.find((c): c is ToolCall => c.type === "toolCall");
|
||||
|
||||
if (toolCall && toolCall.name && functionsEnabled) {
|
||||
const gigaToolName = rememberToolNameMapping(
|
||||
toolNameToGiga,
|
||||
gigaToToolName,
|
||||
toolCall.name,
|
||||
);
|
||||
messages.push({
|
||||
role: "assistant",
|
||||
content: text ? sanitizeContent(text) : "",
|
||||
function_call: {
|
||||
name: mapToolNameToGigaChat(toolCall.name),
|
||||
name: gigaToolName,
|
||||
arguments: toolCall.arguments ?? {},
|
||||
},
|
||||
});
|
||||
@ -479,10 +521,11 @@ export function createGigachatStreamFn(opts: GigachatStreamOptions): StreamFn {
|
||||
sanitizeContent(resultContent || "ok"),
|
||||
toolName,
|
||||
);
|
||||
const gigaToolName = rememberToolNameMapping(toolNameToGiga, gigaToToolName, toolName);
|
||||
messages.push({
|
||||
role: "function",
|
||||
content: coercedContent,
|
||||
name: mapToolNameToGigaChat(toolName),
|
||||
name: gigaToolName,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -511,8 +554,11 @@ export function createGigachatStreamFn(opts: GigachatStreamOptions): StreamFn {
|
||||
logSchemaModifications(tool.name, modifications);
|
||||
|
||||
// Sanitize function name and map reserved names
|
||||
const mappedName = mapToolNameToGigaChat(tool.name);
|
||||
const sanitizedName = sanitizeFunctionName(mappedName);
|
||||
const sanitizedName = rememberToolNameMapping(
|
||||
toolNameToGiga,
|
||||
gigaToToolName,
|
||||
tool.name,
|
||||
);
|
||||
if (sanitizedName !== tool.name) {
|
||||
log.debug(`GigaChat: sanitized function name "${tool.name}" → "${sanitizedName}"`);
|
||||
}
|
||||
@ -543,7 +589,7 @@ export function createGigachatStreamFn(opts: GigachatStreamOptions): StreamFn {
|
||||
|
||||
// Set credentials based on auth mode
|
||||
if (isUserPassCredentials) {
|
||||
const [user, password] = apiKey.split(":", 2);
|
||||
const { user, password } = parseGigachatBasicCredentials(apiKey);
|
||||
clientConfig.user = user;
|
||||
clientConfig.password = password;
|
||||
log.debug(`GigaChat auth: basic mode`);
|
||||
@ -700,7 +746,9 @@ export function createGigachatStreamFn(opts: GigachatStreamOptions): StreamFn {
|
||||
{ cause: parseErr },
|
||||
);
|
||||
}
|
||||
const clientName = mapToolNameFromGigaChat(functionCallBuffer.name);
|
||||
const clientName =
|
||||
gigaToToolName.get(functionCallBuffer.name) ??
|
||||
mapToolNameFromGigaChat(functionCallBuffer.name);
|
||||
accumulatedToolCalls.push({
|
||||
type: "toolCall",
|
||||
id: randomUUID(),
|
||||
|
||||
@ -320,6 +320,28 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("infers GigaChat auth from --gigachat-api-key and stores personal OAuth metadata", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-gigachat-infer-", async (env) => {
|
||||
const cfg = await runOnboardingAndReadConfig(env, {
|
||||
gigachatApiKey: "gigachat-credentials==", // pragma: allowlist secret
|
||||
});
|
||||
|
||||
expect(cfg.auth?.profiles?.["gigachat:default"]?.provider).toBe("gigachat");
|
||||
expect(cfg.auth?.profiles?.["gigachat:default"]?.mode).toBe("api_key");
|
||||
expect(cfg.agents?.defaults?.model?.primary).toBe("gigachat/GigaChat-2-Max");
|
||||
await expectApiKeyProfile({
|
||||
profileId: "gigachat:default",
|
||||
provider: "gigachat",
|
||||
key: "gigachat-credentials==",
|
||||
metadata: {
|
||||
authMode: "oauth",
|
||||
scope: "GIGACHAT_API_PERS",
|
||||
insecureTls: "false",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("stores Volcano Engine API key and sets default model", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-volcengine-", async (env) => {
|
||||
const cfg = await runOnboardingAndReadConfig(env, {
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
applyKilocodeConfig,
|
||||
applyKimiCodeConfig,
|
||||
applyLitellmConfig,
|
||||
applyGigachatConfig,
|
||||
applyMistralConfig,
|
||||
applyModelStudioConfig,
|
||||
applyModelStudioConfigCn,
|
||||
@ -29,6 +30,7 @@ import {
|
||||
setHuggingfaceApiKey,
|
||||
setKilocodeApiKey,
|
||||
setKimiCodingApiKey,
|
||||
setGigachatApiKey,
|
||||
setLitellmApiKey,
|
||||
setMistralApiKey,
|
||||
setModelStudioApiKey,
|
||||
@ -149,6 +151,28 @@ function buildSimpleApiKeyAuthChoices(params: { opts: OnboardOptions }): SimpleA
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
authChoices: ["gigachat-api-key", "gigachat-oauth"],
|
||||
provider: "gigachat",
|
||||
flagValue: params.opts.gigachatApiKey,
|
||||
flagName: "--gigachat-api-key",
|
||||
envVar: "GIGACHAT_CREDENTIALS",
|
||||
profileId: "gigachat:default",
|
||||
setCredential: (value, options) =>
|
||||
setGigachatApiKey(value, undefined, options, {
|
||||
authMode: "oauth",
|
||||
scope: "GIGACHAT_API_PERS",
|
||||
insecureTls: "false",
|
||||
}),
|
||||
applyConfig: (cfg) =>
|
||||
applyGigachatConfig(
|
||||
applyAuthProfileConfig(cfg, {
|
||||
profileId: "gigachat:default",
|
||||
provider: "gigachat",
|
||||
mode: "api_key",
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
authChoices: ["mistral-api-key"],
|
||||
provider: "mistral",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user