fix(studio): make RightCode custom chat work end-to-end

This commit is contained in:
Ma
2026-04-15 15:46:17 +08:00
parent cbf89bbda4
commit 396002cb43
3 changed files with 74 additions and 4 deletions
+3 -2
View File
@@ -144,11 +144,13 @@ export function createLLMClient(config: LLMConfig): LLMClient {
const baseUrl = config.baseUrl || preset?.baseUrl || "";
const extraHeaders = config.headers ?? parseEnvHeaders();
const provider = config.provider === "anthropic" ? "anthropic" : "openai";
const piModel: PiModel<PiApi> = {
id: config.model,
name: config.model,
api: piApi,
provider: serviceName,
provider,
baseUrl,
reasoning: (config.thinkingBudget ?? 0) > 0,
input: ["text"] as ("text" | "image")[],
@@ -158,7 +160,6 @@ export function createLLMClient(config: LLMConfig): LLMClient {
...(extraHeaders ? { headers: extraHeaders } : {}),
};
const provider = config.provider === "anthropic" ? "anthropic" : "openai";
return {
provider,
apiFormat,
+28 -1
View File
@@ -122,6 +122,7 @@ vi.mock("@actalk/inkos-core", () => {
loadProjectSession: loadProjectSessionMock,
resolveSessionActiveBook: resolveSessionActiveBookMock,
runAgentSession: runAgentSessionMock,
buildAgentSystemPrompt: vi.fn(() => "You are helpful."),
findOrCreateBookSession: findOrCreateBookSessionMock,
loadBookSession: loadBookSessionMock,
persistBookSession: persistBookSessionMock,
@@ -989,7 +990,7 @@ describe("createStudioServer daemon lifecycle", () => {
responseText: "",
messages: [{ role: "user", content: "nihao" }],
});
chatCompletionMock.mockRejectedValueOnce(new Error("quota exhausted"));
chatCompletionMock.mockRejectedValue(new Error("quota exhausted"));
const { createStudioServer } = await import("./server.js");
const app = createStudioServer(cloneProjectConfig() as never, root);
@@ -1010,6 +1011,32 @@ describe("createStudioServer daemon lifecycle", () => {
});
});
it("falls back to plain chat when the tool-agent returns empty text", async () => {
runAgentSessionMock.mockResolvedValueOnce({
responseText: "",
messages: [{ role: "user", content: "nihao" }],
});
chatCompletionMock.mockResolvedValueOnce({
content: "你好!",
usage: { promptTokens: 1, completionTokens: 1, totalTokens: 2 },
});
const { createStudioServer } = await import("./server.js");
const app = createStudioServer(cloneProjectConfig() as never, root);
const response = await app.request("http://localhost/api/v1/agent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ instruction: "nihao", activeBookId: "demo-book" }),
});
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({
response: "你好!",
session: { sessionId: "agent-session-1" },
});
});
it("returns the shared interaction session state", async () => {
loadProjectSessionMock.mockResolvedValue({
sessionId: "session-2",
+43 -1
View File
@@ -19,6 +19,7 @@ import {
persistBookSession,
appendBookSessionMessage,
runAgentSession,
buildAgentSystemPrompt,
resolveServicePreset,
resolveServiceModel,
loadSecrets,
@@ -812,7 +813,13 @@ export function createStudioServer(initialConfig: ProjectConfig, root: string) {
return c.json({ ok: false, error: "连接成功但未返回可用模型" }, 400);
}
const sampleModel = models[0]?.id;
const rawConfig = await loadRawConfig(root).catch(() => ({} as Record<string, unknown>));
const preferredModel = typeof (rawConfig.llm as Record<string, unknown> | undefined)?.defaultModel === "string"
? String((rawConfig.llm as Record<string, unknown>).defaultModel)
: undefined;
const sampleModel = models.find((m) => m.id === preferredModel)?.id
?? models.find((m) => m.id === "gpt-5.4")?.id
?? models[0]?.id;
if (sampleModel) {
const client = createLLMClient({
provider: service === "anthropic" ? "anthropic" : "openai",
@@ -1310,6 +1317,41 @@ export function createStudioServer(initialConfig: ProjectConfig, root: string) {
});
}
if (!result.responseText) {
try {
const fallbackClient = createLLMClient({
...config.llm,
service: configuredEntry?.service ?? reqService ?? config.llm.service,
model: reqModel ?? config.llm.model,
apiKey: agentApiKey ?? config.llm.apiKey,
baseUrl: configuredEntry?.baseUrl ?? config.llm.baseUrl,
...(configuredEntry?.apiFormat ? { apiFormat: configuredEntry.apiFormat } : {}),
...(configuredEntry?.stream !== undefined ? { stream: configuredEntry.stream } : {}),
} as ProjectConfig["llm"]);
const fallback = await chatCompletion(
fallbackClient,
reqModel ?? config.llm.model,
[
{ role: "system", content: buildAgentSystemPrompt(activeBookId ?? null, config.language ?? "zh") },
{ role: "user", content: instruction },
],
{ maxTokens: 256 },
);
if (fallback.content?.trim()) {
bookSession = appendBookSessionMessage(bookSession, {
role: "assistant",
content: fallback.content,
timestamp: Date.now() + 1,
});
await persistBookSession(root, bookSession);
return c.json({
response: fallback.content,
session: { sessionId: bookSession.sessionId },
});
}
} catch {
// fall through to probe-based diagnosis below
}
try {
const probeClient = createLLMClient({
...config.llm,