@musnows/scriverse 0.5.3 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,7 +41,7 @@
41
41
  - 大纲与伏笔:维护章节目标、冲突、转折和伏笔的埋设、提醒与回收。
42
42
  - AI 创作助手:支持 Markdown 和流式输出,可引用章节行、附加角色与设定上下文。
43
43
  - AI 任务:结构分析、章节分析、角色抽取、时间线分析、关系分析和一致性检查。
44
- - 供应商管理:兼容 OpenAI Chat Completions 协议,可配置模型、最大输出 Token、并发数和 RPM。
44
+ - 供应商管理:兼容 OpenAI Chat Completions 与 Anthropic Messages 协议,可配置模型、最大输出 Token、并发数和 RPM。
45
45
  - 安全导出:支持 JSON、TXT 和 Markdown,导出内容不包含 AI 密钥。
46
46
 
47
47
  ## 技术栈
@@ -141,7 +141,7 @@ npm start
141
141
  配置前请先阅读 [AI 供应商兼容性与配置指南](docs/AI-PROVIDER-COMPATIBILITY.md),其中列出了已验证的服务商、基础地址、模型标识符和已知差异。
142
142
 
143
143
  1. 启动项目后,点击顶部“AI 管理”进入平台级配置。
144
- 2. 新建兼容 OpenAI Chat Completions 的供应商,填写基础地址、API 密钥、并发数、RPM 与最大输出 Token。
144
+ 2. 新建兼容 OpenAI Chat Completions 或 Anthropic Messages 的供应商,选择协议并填写基础地址、API 密钥、并发数、RPM 与最大输出 Token。
145
145
  3. 为模型填写其支持的上下文总量(Token),再添加模型。
146
146
  4. 在平台页设置全局系统提示词;它会追加在内置提示词之后。
147
147
  5. 打开一本作品,在“更多 → AI 设置”中设置该书的追加系统提示词和任务默认模型;书籍提示词会追加在全局提示词之后。
@@ -216,7 +216,8 @@ curl http://127.0.0.1:13210/api/health
216
216
  "data": {
217
217
  "status": "ok",
218
218
  "version": "0.3.3",
219
- "protocol": "openai-chat-completions"
219
+ "protocol": "openai-chat-completions",
220
+ "protocols": ["openai-chat-completions", "anthropic-messages"]
220
221
  }
221
222
  }
222
223
  ```
@@ -0,0 +1,216 @@
1
+ import { normalizeBaseUrl } from "./utils.js";
2
+ export const AI_PROVIDER_PROTOCOLS = ["openai-chat-completions", "anthropic-messages"];
3
+ export function normalizeProviderBaseUrl(value) {
4
+ return normalizeBaseUrl(value).replace(/\/messages$/u, "");
5
+ }
6
+ function appendVersionedResource(baseUrl, resource) {
7
+ const normalized = normalizeProviderBaseUrl(baseUrl);
8
+ return /\/v1$/u.test(normalized) ? `${normalized}/${resource}` : `${normalized}/v1/${resource}`;
9
+ }
10
+ export function providerCompletionEndpoint(baseUrl, protocol) {
11
+ const normalized = normalizeProviderBaseUrl(baseUrl);
12
+ return protocol === "anthropic-messages"
13
+ ? appendVersionedResource(normalized, "messages")
14
+ : `${normalized}/chat/completions`;
15
+ }
16
+ export function providerModelEndpoints(baseUrl, protocol) {
17
+ const normalized = normalizeProviderBaseUrl(baseUrl);
18
+ if (protocol === "openai-chat-completions")
19
+ return [`${normalized}/models`];
20
+ const primary = appendVersionedResource(normalized, "models");
21
+ const root = new URL("/v1/models", normalized).toString();
22
+ return primary === root ? [primary] : [primary, root];
23
+ }
24
+ export function providerRequestHeaders(protocol, apiKey, accept) {
25
+ return {
26
+ Authorization: `Bearer ${apiKey}`,
27
+ ...(protocol === "anthropic-messages" ? { "x-api-key": apiKey, "anthropic-version": "2023-06-01" } : {}),
28
+ "Content-Type": "application/json",
29
+ Accept: accept
30
+ };
31
+ }
32
+ function textContent(value) {
33
+ return typeof value === "string" && value.length > 0 ? [{ type: "text", text: value }] : [];
34
+ }
35
+ function parsedToolInput(value) {
36
+ if (value && typeof value === "object" && !Array.isArray(value))
37
+ return value;
38
+ if (typeof value !== "string")
39
+ return {};
40
+ try {
41
+ const parsed = JSON.parse(value);
42
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
43
+ }
44
+ catch {
45
+ return {};
46
+ }
47
+ }
48
+ function anthropicAssistantContent(message) {
49
+ if (Array.isArray(message.anthropic_content) && message.anthropic_content.length > 0) {
50
+ return structuredClone(message.anthropic_content);
51
+ }
52
+ return [
53
+ ...textContent(message.content),
54
+ ...message.tool_calls.map((toolCall) => ({
55
+ type: "tool_use",
56
+ id: toolCall.id,
57
+ name: toolCall.function.name,
58
+ input: parsedToolInput(toolCall.function.arguments)
59
+ }))
60
+ ];
61
+ }
62
+ function anthropicToolResult(message) {
63
+ let isError = false;
64
+ try {
65
+ const result = JSON.parse(message.content);
66
+ isError = result.ok === false;
67
+ }
68
+ catch {
69
+ isError = false;
70
+ }
71
+ return {
72
+ type: "tool_result",
73
+ tool_use_id: message.tool_call_id,
74
+ content: message.content,
75
+ ...(isError ? { is_error: true } : {})
76
+ };
77
+ }
78
+ function anthropicMessages(messages) {
79
+ const system = messages
80
+ .filter((message) => message.role === "system")
81
+ .map((message) => message.content)
82
+ .filter(Boolean)
83
+ .join("\n\n");
84
+ const output = [];
85
+ const append = (role, content) => {
86
+ if (content.length === 0)
87
+ return;
88
+ const previous = output.at(-1);
89
+ if (previous?.role === role)
90
+ previous.content.push(...content);
91
+ else
92
+ output.push({ role, content });
93
+ };
94
+ for (const message of messages) {
95
+ if (message.role === "system")
96
+ continue;
97
+ if (message.role === "tool") {
98
+ append("user", [anthropicToolResult(message)]);
99
+ continue;
100
+ }
101
+ if (message.role === "assistant" && "tool_calls" in message) {
102
+ append("assistant", anthropicAssistantContent(message));
103
+ continue;
104
+ }
105
+ append(message.role, textContent(message.content));
106
+ }
107
+ return { ...(system ? { system } : {}), messages: output };
108
+ }
109
+ function anthropicTools(tools) {
110
+ return tools.flatMap((tool) => {
111
+ const fn = tool.function && typeof tool.function === "object" && !Array.isArray(tool.function)
112
+ ? tool.function
113
+ : null;
114
+ if (!fn || typeof fn.name !== "string")
115
+ return [];
116
+ return [{
117
+ name: fn.name,
118
+ ...(typeof fn.description === "string" ? { description: fn.description } : {}),
119
+ input_schema: fn.parameters && typeof fn.parameters === "object" && !Array.isArray(fn.parameters)
120
+ ? fn.parameters
121
+ : { type: "object", properties: {} }
122
+ }];
123
+ });
124
+ }
125
+ export function buildCompletionRequestBody(input) {
126
+ const tools = input.toolChoice === "auto" ? input.tools ?? [] : [];
127
+ if (input.protocol === "openai-chat-completions") {
128
+ return {
129
+ model: input.model,
130
+ messages: input.messages,
131
+ ...input.parameters,
132
+ ...(tools.length > 0 ? { tools, tool_choice: "auto" } : {}),
133
+ ...(input.stream ? { stream: true, stream_options: { include_usage: true } } : {})
134
+ };
135
+ }
136
+ const translated = anthropicMessages(input.messages);
137
+ const parameters = Object.fromEntries(Object.entries(input.parameters)
138
+ .filter(([key]) => ["temperature", "top_p", "max_tokens", "thinking"].includes(key)));
139
+ return {
140
+ model: input.model,
141
+ ...translated,
142
+ ...parameters,
143
+ ...(tools.length > 0 ? { tools: anthropicTools(tools), tool_choice: { type: "auto" } } : {}),
144
+ ...(input.stream ? { stream: true } : {})
145
+ };
146
+ }
147
+ function replayableAnthropicBlock(value) {
148
+ if (!value || typeof value !== "object" || Array.isArray(value))
149
+ return null;
150
+ const block = value;
151
+ if (block.type === "text" && typeof block.text === "string")
152
+ return { type: "text", text: block.text };
153
+ if (block.type === "thinking" && typeof block.thinking === "string" && typeof block.signature === "string") {
154
+ return { type: "thinking", thinking: block.thinking, signature: block.signature };
155
+ }
156
+ if (block.type === "redacted_thinking" && typeof block.data === "string") {
157
+ return { type: "redacted_thinking", data: block.data };
158
+ }
159
+ if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
160
+ return { type: "tool_use", id: block.id, name: block.name, input: parsedToolInput(block.input) };
161
+ }
162
+ return null;
163
+ }
164
+ function anthropicFinishReason(value) {
165
+ if (value === "max_tokens")
166
+ return "length";
167
+ return typeof value === "string" ? value : null;
168
+ }
169
+ export function parseCompletionPayload(protocol, value) {
170
+ if (protocol === "openai-chat-completions") {
171
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
172
+ }
173
+ const response = value && typeof value === "object" && !Array.isArray(value) ? value : {};
174
+ const content = Array.isArray(response.content) ? response.content : [];
175
+ const replay = content.map(replayableAnthropicBlock).filter((block) => block !== null);
176
+ const text = content.flatMap((value) => {
177
+ const block = value && typeof value === "object" && !Array.isArray(value) ? value : {};
178
+ return block.type === "text" && typeof block.text === "string" ? [block.text] : [];
179
+ }).join("");
180
+ const reasoning = content.flatMap((value) => {
181
+ const block = value && typeof value === "object" && !Array.isArray(value) ? value : {};
182
+ if (block.type === "thinking" && typeof block.thinking === "string")
183
+ return [block.thinking];
184
+ if (block.type === "text" && typeof block.thinking === "string")
185
+ return [block.thinking];
186
+ return [];
187
+ }).join("");
188
+ const toolCalls = content.flatMap((value) => {
189
+ const block = value && typeof value === "object" && !Array.isArray(value) ? value : {};
190
+ if (block.type !== "tool_use" || typeof block.id !== "string" || typeof block.name !== "string")
191
+ return [];
192
+ return [{
193
+ id: block.id,
194
+ type: "function",
195
+ function: {
196
+ name: block.name,
197
+ arguments: parsedToolInput(block.input)
198
+ }
199
+ }];
200
+ });
201
+ return {
202
+ ...(response.usage && typeof response.usage === "object" && !Array.isArray(response.usage)
203
+ ? { usage: response.usage }
204
+ : {}),
205
+ choices: [{
206
+ finish_reason: anthropicFinishReason(response.stop_reason),
207
+ message: {
208
+ content: text || null,
209
+ reasoning_content: reasoning || null,
210
+ tool_calls: toolCalls,
211
+ anthropic_content: replay
212
+ }
213
+ }]
214
+ };
215
+ }
216
+ //# sourceMappingURL=ai-protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-protocol.js","sourceRoot":"","sources":["../src/ai-protocol.ts"],"names":[],"mappings":"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"}
package/dist/ai.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { buildCompletionRequestBody, normalizeProviderBaseUrl, parseCompletionPayload, providerCompletionEndpoint, providerModelEndpoints, providerRequestHeaders } from "./ai-protocol.js";
1
2
  import { PLATFORM_AI_WORK_ID } from "./database.js";
2
3
  import { AppError, notFound } from "./errors.js";
3
4
  import { logger, sanitizeError } from "./logger.js";
@@ -5,7 +6,7 @@ import { paginated, paginationSql } from "./pagination.js";
5
6
  import { currentRequestActor } from "./request-context.js";
6
7
  import { fetchSafeAiEndpoint } from "./security.js";
7
8
  import { RELATIONSHIP_SEARCH_POLICY_VERSION, RelationshipApproximateMatchLimitError, findApproximateNameMatchesChunked, ftsPhrase, normalizeRelationshipSearchText, relationshipCharacterTokenText, relationshipCharacterTokens, relationshipPinyinTokenText, relationshipPinyinTokens } from "./relationship-search.js";
8
- import { clamp, id, json, maskSecret, normalizeBaseUrl, now } from "./utils.js";
9
+ import { clamp, id, json, maskSecret, now } from "./utils.js";
9
10
  import { z } from "zod";
10
11
  export function aiErrorForLog(error) {
11
12
  const sanitized = sanitizeError(error);
@@ -33,9 +34,22 @@ function isGeminiProviderOrModel(provider, model) {
33
34
  function isKimiModelId(modelId) {
34
35
  return modelId.toLowerCase().includes("kimi");
35
36
  }
37
+ function providerProtocol(provider) {
38
+ return stringValue(provider, "protocol") === "anthropic-messages" ? "anthropic-messages" : "openai-chat-completions";
39
+ }
40
+ function isLongCatProvider(provider) {
41
+ try {
42
+ return new URL(stringValue(provider, "base_url")).hostname.toLowerCase() === "api.longcat.chat";
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
36
48
  function thinkingParameters(provider, model) {
37
49
  if (isGeminiProviderOrModel(provider, model))
38
50
  return {};
51
+ if (providerProtocol(provider) === "anthropic-messages" && !isLongCatProvider(provider))
52
+ return {};
39
53
  return { thinking: { type: boolValue(model, "thinking_enabled") ? "enabled" : "disabled" } };
40
54
  }
41
55
  const AGENT_TOOL_IDS = ["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections"];
@@ -990,10 +1004,12 @@ export class AiManager {
990
1004
  const providerId = id("provider");
991
1005
  const encrypted = this.vault.encrypt(input.apiKey);
992
1006
  const timestamp = now();
993
- this.store.db.run(`INSERT INTO providers (id, work_id, name, base_url, encrypted_key, key_iv, key_tag, key_hint, status,
1007
+ const protocol = input.protocol ?? "openai-chat-completions";
1008
+ const baseUrl = normalizeProviderBaseUrl(input.baseUrl);
1009
+ this.store.db.run(`INSERT INTO providers (id, work_id, name, base_url, protocol, encrypted_key, key_iv, key_tag, key_hint, status,
994
1010
  connection_status, concurrency_limit, rpm_limit, max_tokens, note, created_at, updated_at)
995
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'unchecked', ?, ?, ?, ?, ?, ?)`, providerId, PLATFORM_AI_WORK_ID, input.name, normalizeBaseUrl(input.baseUrl), encrypted.encrypted, encrypted.iv, encrypted.tag, maskSecret(input.apiKey), input.status ?? "disabled", input.concurrencyLimit ?? 10, input.rpmLimit ?? 10, input.maxTokens ?? DEFAULT_MAX_TOKENS, input.note ?? "", timestamp, timestamp);
996
- this.store.audit(PLATFORM_AI_WORK_ID, "provider.created", "provider", providerId, { name: input.name, baseUrl: normalizeBaseUrl(input.baseUrl) });
1011
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'unchecked', ?, ?, ?, ?, ?, ?)`, providerId, PLATFORM_AI_WORK_ID, input.name, baseUrl, protocol, encrypted.encrypted, encrypted.iv, encrypted.tag, maskSecret(input.apiKey), input.status ?? "disabled", input.concurrencyLimit ?? 10, input.rpmLimit ?? 10, input.maxTokens ?? DEFAULT_MAX_TOKENS, input.note ?? "", timestamp, timestamp);
1012
+ this.store.audit(PLATFORM_AI_WORK_ID, "provider.created", "provider", providerId, { name: input.name, baseUrl, protocol });
997
1013
  return this.getProvider(providerId);
998
1014
  }
999
1015
  listProviders() {
@@ -1022,10 +1038,12 @@ export class AiManager {
1022
1038
  keyHint = maskSecret(input.apiKey);
1023
1039
  connectionStatus = "unchecked";
1024
1040
  }
1025
- if (input.baseUrl && normalizeBaseUrl(input.baseUrl) !== stringValue(row, "base_url"))
1041
+ if (input.baseUrl && normalizeProviderBaseUrl(input.baseUrl) !== stringValue(row, "base_url"))
1042
+ connectionStatus = "unchecked";
1043
+ if (input.protocol && input.protocol !== providerProtocol(row))
1026
1044
  connectionStatus = "unchecked";
1027
- this.store.db.run(`UPDATE providers SET name = ?, base_url = ?, encrypted_key = ?, key_iv = ?, key_tag = ?, key_hint = ?,
1028
- status = ?, connection_status = ?, concurrency_limit = ?, rpm_limit = ?, max_tokens = ?, note = ?, updated_at = ? WHERE id = ?`, input.name ?? stringValue(row, "name"), input.baseUrl ? normalizeBaseUrl(input.baseUrl) : stringValue(row, "base_url"), encryptedKey, keyIv, keyTag, keyHint, input.status ?? stringValue(row, "status"), connectionStatus, input.concurrencyLimit ?? numberValue(row, "concurrency_limit"), input.rpmLimit ?? numberValue(row, "rpm_limit"), input.maxTokens ?? numberValue(row, "max_tokens"), input.note ?? stringValue(row, "note"), now(), providerId);
1045
+ this.store.db.run(`UPDATE providers SET name = ?, base_url = ?, protocol = ?, encrypted_key = ?, key_iv = ?, key_tag = ?, key_hint = ?,
1046
+ status = ?, connection_status = ?, concurrency_limit = ?, rpm_limit = ?, max_tokens = ?, note = ?, updated_at = ? WHERE id = ?`, input.name ?? stringValue(row, "name"), input.baseUrl ? normalizeProviderBaseUrl(input.baseUrl) : stringValue(row, "base_url"), input.protocol ?? providerProtocol(row), encryptedKey, keyIv, keyTag, keyHint, input.status ?? stringValue(row, "status"), connectionStatus, input.concurrencyLimit ?? numberValue(row, "concurrency_limit"), input.rpmLimit ?? numberValue(row, "rpm_limit"), input.maxTokens ?? numberValue(row, "max_tokens"), input.note ?? stringValue(row, "note"), now(), providerId);
1029
1047
  this.store.audit(PLATFORM_AI_WORK_ID, "provider.updated", "provider", providerId, {
1030
1048
  fields: Object.keys(input).filter((key) => key !== "apiKey"),
1031
1049
  keyReplaced: Boolean(input.apiKey)
@@ -1051,26 +1069,40 @@ export class AiManager {
1051
1069
  async testProvider(providerId) {
1052
1070
  const row = this.getProviderRow(providerId);
1053
1071
  const apiKey = this.decryptKey(row);
1072
+ const protocol = providerProtocol(row);
1054
1073
  const controller = new AbortController();
1055
1074
  const timeout = setTimeout(() => controller.abort(), 10_000);
1056
1075
  const startedAt = process.hrtime.bigint();
1057
1076
  logger.info("ai.provider_test.started", { providerId });
1058
1077
  try {
1059
- const endpoint = `${normalizeBaseUrl(stringValue(row, "base_url"))}/models`;
1060
- const response = await this.outboundFetch(endpoint, {
1061
- headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
1062
- signal: controller.signal
1063
- });
1064
- if (!response.ok) {
1078
+ let payload = null;
1079
+ let lastFailure = "AI 供应商没有返回模型列表";
1080
+ const endpoints = providerModelEndpoints(stringValue(row, "base_url"), protocol);
1081
+ for (let index = 0; index < endpoints.length; index += 1) {
1082
+ const endpoint = endpoints[index];
1083
+ if (!endpoint)
1084
+ continue;
1085
+ const response = await this.outboundFetch(endpoint, {
1086
+ headers: providerRequestHeaders(protocol, apiKey, "application/json"),
1087
+ signal: controller.signal
1088
+ });
1089
+ if (response.ok) {
1090
+ payload = (await response.json());
1091
+ break;
1092
+ }
1065
1093
  const message = await response.text();
1066
- throw new Error(`HTTP ${response.status}: ${message.slice(0, 300)}`);
1094
+ lastFailure = `HTTP ${response.status}: ${message.slice(0, 300)}`;
1095
+ if (response.status !== 404 || index === endpoints.length - 1)
1096
+ break;
1067
1097
  }
1068
- const payload = (await response.json());
1098
+ if (!payload)
1099
+ throw new Error(lastFailure);
1069
1100
  const availableModels = Array.isArray(payload.data) ? payload.data.map((item) => item.id).filter(Boolean) : [];
1070
1101
  const timestamp = now();
1071
1102
  this.store.db.run("UPDATE providers SET connection_status = 'success', last_error = NULL, last_success_at = ?, updated_at = ? WHERE id = ?", timestamp, timestamp, providerId);
1072
1103
  logger.info("ai.provider_test.completed", {
1073
1104
  providerId,
1105
+ protocol,
1074
1106
  ok: true,
1075
1107
  availableModelCount: availableModels.length,
1076
1108
  durationMs: Number(process.hrtime.bigint() - startedAt) / 1_000_000
@@ -1078,10 +1110,11 @@ export class AiManager {
1078
1110
  return { ok: true, availableModels, provider: this.getProvider(providerId) };
1079
1111
  }
1080
1112
  catch (error) {
1081
- const message = error instanceof Error ? error.message : "连接失败";
1113
+ const message = error instanceof Error ? redactProviderSecret(error.message, apiKey) : "连接失败";
1082
1114
  this.store.db.run("UPDATE providers SET connection_status = 'failed', last_error = ?, updated_at = ? WHERE id = ?", message, now(), providerId);
1083
1115
  logger.warn("ai.provider_test.completed", {
1084
1116
  providerId,
1117
+ protocol,
1085
1118
  ok: false,
1086
1119
  durationMs: Number(process.hrtime.bigint() - startedAt) / 1_000_000,
1087
1120
  error: aiErrorForLog(error)
@@ -2028,12 +2061,14 @@ export class AiManager {
2028
2061
  this.store.db.run("UPDATE ai_call_traces SET rounds_json = ?, source_refs_json = ?, updated_at = ? WHERE call_id = ?", JSON.stringify(traceRounds), JSON.stringify(taskTraceSourceRefs(messages, traceRounds)), now(), callId);
2029
2062
  };
2030
2063
  const callStartedAt = process.hrtime.bigint();
2064
+ const protocol = providerProtocol(provider);
2031
2065
  logger.info("ai.call.started", {
2032
2066
  callId,
2033
2067
  workId: input.workId,
2034
2068
  taskType: input.taskType,
2035
2069
  providerId: stringValue(provider, "id"),
2036
2070
  modelId: stringValue(model, "id"),
2071
+ protocol,
2037
2072
  streaming: false,
2038
2073
  contextChars: context.length,
2039
2074
  instructionChars: input.instruction.length,
@@ -2043,7 +2078,7 @@ export class AiManager {
2043
2078
  try {
2044
2079
  const apiKey = this.decryptKey(provider);
2045
2080
  activeApiKey = apiKey;
2046
- const endpoint = `${normalizeBaseUrl(stringValue(provider, "base_url"))}/chat/completions`;
2081
+ const endpoint = providerCompletionEndpoint(stringValue(provider, "base_url"), protocol);
2047
2082
  const timeoutMs = input.taskType === "book-analysis" || input.taskType === "relationship-analysis" ? 300_000 : 60_000;
2048
2083
  const maximumAttempts = Math.round(clamp(input.maxAttempts ?? 3, 1, 5));
2049
2084
  let completionRequestCount = 0;
@@ -2090,13 +2125,15 @@ export class AiManager {
2090
2125
  try {
2091
2126
  const response = await this.outboundFetch(endpoint, {
2092
2127
  method: "POST",
2093
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", Accept: "application/json" },
2094
- body: JSON.stringify({
2128
+ headers: providerRequestHeaders(protocol, apiKey, "application/json"),
2129
+ body: JSON.stringify(buildCompletionRequestBody({
2130
+ protocol,
2095
2131
  model: stringValue(model, "model_id"),
2096
2132
  messages: completionMessages,
2097
- ...parameters,
2098
- ...(tools.length && toolChoice === "auto" ? { tools, tool_choice: "auto" } : {})
2099
- }),
2133
+ parameters,
2134
+ tools,
2135
+ toolChoice
2136
+ })),
2100
2137
  signal: controller.signal
2101
2138
  });
2102
2139
  return { ok: response.ok, status: response.status, body: await response.text() };
@@ -2115,7 +2152,7 @@ export class AiManager {
2115
2152
  });
2116
2153
  if (candidate.ok) {
2117
2154
  try {
2118
- const parsed = redactProviderSecrets(JSON.parse(candidate.body), apiKey);
2155
+ const parsed = parseCompletionPayload(protocol, redactProviderSecrets(JSON.parse(candidate.body), apiKey));
2119
2156
  traceAttempt.completedAt = now();
2120
2157
  traceAttempt.status = "completed";
2121
2158
  traceAttempt.httpStatus = candidate.status;
@@ -2132,7 +2169,7 @@ export class AiManager {
2132
2169
  return parsed;
2133
2170
  }
2134
2171
  catch {
2135
- throw new Error(`Chat Completions returned invalid JSON: ${candidate.body.slice(0, 500)}`);
2172
+ throw new Error(`${protocol === "anthropic-messages" ? "Anthropic Messages" : "Chat Completions"} returned invalid JSON: ${candidate.body.slice(0, 500)}`);
2136
2173
  }
2137
2174
  }
2138
2175
  lastFailure = new Error(`HTTP ${candidate.status}: ${candidate.body.slice(0, 500)}`);
@@ -2211,7 +2248,8 @@ export class AiManager {
2211
2248
  role: "assistant",
2212
2249
  content: choice.message.content ?? null,
2213
2250
  reasoning_content: choice.message.reasoning_content ?? null,
2214
- tool_calls: normalizedToolCalls
2251
+ tool_calls: normalizedToolCalls,
2252
+ ...(choice.message.anthropic_content?.length ? { anthropic_content: choice.message.anthropic_content } : {})
2215
2253
  });
2216
2254
  for (const toolCall of toolCalls) {
2217
2255
  const execution = this.executeAgentTool(input.workId, toolCall);
@@ -2244,7 +2282,7 @@ export class AiManager {
2244
2282
  const suffix = choice?.finish_reason === "length" || reasoningLength > 0
2245
2283
  ? `;模型已生成 ${reasoningLength} 个推理字符,请提高 max_tokens 输出预算`
2246
2284
  : "";
2247
- throw new Error(`Chat Completions 响应缺少可用正文,finish_reason=${choice?.finish_reason ?? "unknown"}${suffix}`);
2285
+ throw new Error(`${protocol === "anthropic-messages" ? "Anthropic Messages" : "Chat Completions"} 响应缺少可用正文,finish_reason=${choice?.finish_reason ?? "unknown"}${suffix}`);
2248
2286
  }
2249
2287
  this.store.db.run("UPDATE ai_calls SET status = 'completed', output_chars = ?, completed_at = ? WHERE id = ?", content.length, now(), callId);
2250
2288
  const outputTokens = resolveOutputTokens(payload.usage, content);
@@ -2290,19 +2328,23 @@ export class AiManager {
2290
2328
  this.store.db.run(`INSERT INTO ai_calls (id, work_id, task_type, provider_id, model_id, context_scope_json, parameters_json,
2291
2329
  status, input_chars, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, 'running', ?, ?, ?)`, callId, input.workId, input.taskType, stringValue(provider, "id"), stringValue(model, "id"), JSON.stringify(input.scope), JSON.stringify(parameters), context.length + input.instruction.length, now(), currentRequestActor()?.userId ?? null);
2292
2330
  const callStartedAt = process.hrtime.bigint();
2331
+ const protocol = providerProtocol(provider);
2293
2332
  logger.info("ai.call.started", {
2294
2333
  callId,
2295
2334
  workId: input.workId,
2296
2335
  taskType: input.taskType,
2297
2336
  providerId: stringValue(provider, "id"),
2298
2337
  modelId: stringValue(model, "id"),
2338
+ protocol,
2299
2339
  streaming: true,
2300
2340
  contextChars: context.length,
2301
2341
  instructionChars: input.instruction.length
2302
2342
  });
2343
+ let activeApiKey = "";
2303
2344
  try {
2304
2345
  const apiKey = this.decryptKey(provider);
2305
- const endpoint = `${normalizeBaseUrl(stringValue(provider, "base_url"))}/chat/completions`;
2346
+ activeApiKey = apiKey;
2347
+ const endpoint = providerCompletionEndpoint(stringValue(provider, "base_url"), protocol);
2306
2348
  const maximumAttempts = Math.round(clamp(input.maxAttempts ?? 3, 1, 5));
2307
2349
  let streamedResult = null;
2308
2350
  let lastFailure = null;
@@ -2324,13 +2366,19 @@ export class AiManager {
2324
2366
  try {
2325
2367
  const response = await this.outboundFetch(endpoint, {
2326
2368
  method: "POST",
2327
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", Accept: "text/event-stream" },
2328
- body: JSON.stringify({ model: stringValue(model, "model_id"), messages, ...parameters, stream: true, stream_options: { include_usage: true } }),
2369
+ headers: providerRequestHeaders(protocol, apiKey, "text/event-stream"),
2370
+ body: JSON.stringify(buildCompletionRequestBody({
2371
+ protocol,
2372
+ model: stringValue(model, "model_id"),
2373
+ messages,
2374
+ parameters,
2375
+ stream: true
2376
+ })),
2329
2377
  signal: controller.signal
2330
2378
  });
2331
2379
  if (!response.ok)
2332
2380
  return { ok: false, status: response.status, body: await response.text() };
2333
- const streamed = await this.readCompletionStream(response, (delta) => {
2381
+ const streamed = await this.readCompletionStream(response, protocol, (delta) => {
2334
2382
  emitted = true;
2335
2383
  onDelta(delta);
2336
2384
  }, (delta) => {
@@ -2395,7 +2443,7 @@ export class AiManager {
2395
2443
  return { callId, content, outputTokens, ...(cacheHitPercent === undefined ? {} : { cacheHitPercent }), provider: this.mapProvider(provider), model: this.mapModel(model), context, toolCalls: [], processSteps };
2396
2444
  }
2397
2445
  catch (error) {
2398
- const message = error instanceof Error ? error.message : "AI 流式调用失败";
2446
+ const message = error instanceof Error ? redactProviderSecret(error.message, activeApiKey) : "AI 流式调用失败";
2399
2447
  this.store.db.run("UPDATE ai_calls SET status = 'failed', failure = ?, completed_at = ? WHERE id = ?", message, now(), callId);
2400
2448
  logger.error("ai.call.failed", {
2401
2449
  callId,
@@ -2408,9 +2456,10 @@ export class AiManager {
2408
2456
  throw new AppError(502, "AI_CALL_FAILED", "AI 调用失败", { callId, failure: message });
2409
2457
  }
2410
2458
  }
2411
- async readCompletionStream(response, onDelta, onThinkingDelta) {
2459
+ async readCompletionStream(response, protocol, onDelta, onThinkingDelta) {
2460
+ const protocolLabel = protocol === "anthropic-messages" ? "Anthropic Messages" : "Chat Completions";
2412
2461
  if (!response.body)
2413
- throw new Error("Chat Completions 流式响应缺少正文");
2462
+ throw new Error(`${protocolLabel} 流式响应缺少正文`);
2414
2463
  const reader = response.body.getReader();
2415
2464
  const decoder = new TextDecoder();
2416
2465
  let buffer = "";
@@ -2427,19 +2476,59 @@ export class AiManager {
2427
2476
  if (!data || data === "[DONE]")
2428
2477
  return;
2429
2478
  const payload = JSON.parse(data);
2430
- if (payload.error)
2431
- throw new Error(payload.error.message || "上游流式响应返回错误");
2432
- if (payload.usage)
2433
- usage = payload.usage;
2434
- const choice = payload.choices?.[0];
2435
- if (choice?.finish_reason)
2479
+ const error = payload.error && typeof payload.error === "object" && !Array.isArray(payload.error)
2480
+ ? payload.error
2481
+ : null;
2482
+ if (error)
2483
+ throw new Error(typeof error.message === "string" ? error.message : "上游流式响应返回错误");
2484
+ if (protocol === "anthropic-messages") {
2485
+ const eventUsage = payload.usage && typeof payload.usage === "object" && !Array.isArray(payload.usage)
2486
+ ? payload.usage
2487
+ : null;
2488
+ const message = payload.message && typeof payload.message === "object" && !Array.isArray(payload.message)
2489
+ ? payload.message
2490
+ : null;
2491
+ const messageUsage = message?.usage && typeof message.usage === "object" && !Array.isArray(message.usage)
2492
+ ? message.usage
2493
+ : null;
2494
+ if (eventUsage || messageUsage) {
2495
+ usage = { ...(usage && typeof usage === "object" ? usage : {}), ...(messageUsage ?? {}), ...(eventUsage ?? {}) };
2496
+ }
2497
+ const eventDelta = payload.delta && typeof payload.delta === "object" && !Array.isArray(payload.delta)
2498
+ ? payload.delta
2499
+ : {};
2500
+ if (typeof eventDelta.stop_reason === "string")
2501
+ finishReason = eventDelta.stop_reason;
2502
+ if (eventDelta.type === "thinking_delta" && typeof eventDelta.thinking === "string" && eventDelta.thinking.length > 0) {
2503
+ reasoning += eventDelta.thinking;
2504
+ onThinkingDelta(eventDelta.thinking);
2505
+ }
2506
+ if (eventDelta.type === "text_delta" && typeof eventDelta.text === "string" && eventDelta.text.length > 0) {
2507
+ content += eventDelta.text;
2508
+ onDelta(eventDelta.text);
2509
+ }
2510
+ return;
2511
+ }
2512
+ const streamUsage = payload.usage && typeof payload.usage === "object" && !Array.isArray(payload.usage)
2513
+ ? payload.usage
2514
+ : null;
2515
+ if (streamUsage)
2516
+ usage = streamUsage;
2517
+ const choices = Array.isArray(payload.choices) ? payload.choices : [];
2518
+ const choice = choices[0] && typeof choices[0] === "object" && !Array.isArray(choices[0])
2519
+ ? choices[0]
2520
+ : null;
2521
+ if (typeof choice?.finish_reason === "string")
2436
2522
  finishReason = choice.finish_reason;
2437
- const thinkingDelta = choice?.delta?.reasoning_content;
2523
+ const deltaRecord = choice?.delta && typeof choice.delta === "object" && !Array.isArray(choice.delta)
2524
+ ? choice.delta
2525
+ : {};
2526
+ const thinkingDelta = deltaRecord.reasoning_content;
2438
2527
  if (typeof thinkingDelta === "string" && thinkingDelta.length > 0) {
2439
2528
  reasoning += thinkingDelta;
2440
2529
  onThinkingDelta(thinkingDelta);
2441
2530
  }
2442
- const delta = choice?.delta?.content;
2531
+ const delta = deltaRecord.content;
2443
2532
  if (typeof delta === "string" && delta.length > 0) {
2444
2533
  content += delta;
2445
2534
  onDelta(delta);
@@ -2458,7 +2547,7 @@ export class AiManager {
2458
2547
  if (buffer.trim())
2459
2548
  consumeEvent(buffer);
2460
2549
  if (!content.trim())
2461
- throw new Error(`Chat Completions 流式响应缺少可用正文,finish_reason=${finishReason}`);
2550
+ throw new Error(`${protocolLabel} 流式响应缺少可用正文,finish_reason=${finishReason}`);
2462
2551
  const cacheHitPercent = resolveCacheHitPercent(usage);
2463
2552
  return { content, reasoning, outputTokens: resolveOutputTokens(usage, content), ...(cacheHitPercent === undefined ? {} : { cacheHitPercent }) };
2464
2553
  }
@@ -5610,6 +5699,7 @@ export class AiManager {
5610
5699
  scope: "platform",
5611
5700
  name: stringValue(row, "name"),
5612
5701
  baseUrl: stringValue(row, "base_url"),
5702
+ protocol: providerProtocol(row),
5613
5703
  apiKey: stringValue(row, "key_hint"),
5614
5704
  status: stringValue(row, "status"),
5615
5705
  connectionStatus: stringValue(row, "connection_status"),