@downcity/agent 1.1.64 → 1.1.66
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/bin/model/CityModelAdapter.d.ts +4 -3
- package/bin/model/CityModelAdapter.d.ts.map +1 -1
- package/bin/model/CityModelAdapter.js +24 -410
- package/bin/model/CityModelAdapter.js.map +1 -1
- package/package.json +3 -2
- package/scripts/city-model-tool-loop.test.mjs +154 -44
- package/src/model/CityModelAdapter.ts +26 -501
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,528 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CityModel 到 AI SDK LanguageModel
|
|
2
|
+
* CityModel 到 AI SDK LanguageModel 的归一化模块。
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
|
-
* - Agent 对外可以接收 CityModel,但 executor
|
|
6
|
-
* -
|
|
5
|
+
* - Agent 对外可以接收 CityModel,但 executor 内部只处理 AI SDK LanguageModel。
|
|
6
|
+
* - CityModel 保留模型目录信息;运行时连接信息通过隐藏协议提供。
|
|
7
|
+
* - 这里直接创建 OpenAI-compatible LanguageModel,不再保留旧的 text/stream 反向适配。
|
|
7
8
|
* - 这里不依赖 @downcity/city,只依赖 @downcity/type 的共享协议。
|
|
8
9
|
*/
|
|
9
10
|
|
|
11
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
10
12
|
import {
|
|
11
13
|
CITY_MODEL_INVOKER,
|
|
12
14
|
isCityModel,
|
|
13
15
|
type CityModel,
|
|
14
|
-
type CityModelInvokeInput,
|
|
15
16
|
} from "@downcity/type";
|
|
16
|
-
import type { LanguageModel
|
|
17
|
+
import type { LanguageModel } from "ai";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Agent SDK 可接受的模型输入。
|
|
20
21
|
*/
|
|
21
22
|
export type AgentModel = LanguageModel | CityModel;
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*/
|
|
27
|
-
role?: string;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* 模型消息内容。
|
|
31
|
-
*/
|
|
32
|
-
content?: unknown;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
type ProviderStreamController = ReadableStreamDefaultController<Record<string, unknown>>;
|
|
36
|
-
type ProviderContentPart = Record<string, unknown>;
|
|
37
|
-
type ProviderPromptRole = "system" | "user" | "assistant" | "tool";
|
|
38
|
-
|
|
39
|
-
type ProviderToolResultOutput = {
|
|
40
|
-
/**
|
|
41
|
-
* tool result 输出类型。
|
|
42
|
-
*/
|
|
43
|
-
type?: unknown;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* tool result 输出值。
|
|
47
|
-
*/
|
|
48
|
-
value?: unknown;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* tool 拒绝原因。
|
|
52
|
-
*/
|
|
53
|
-
reason?: unknown;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
function normalizeFinishReason(input: unknown): {
|
|
57
|
-
unified: "stop" | "length" | "content-filter" | "tool-calls" | "error" | "other";
|
|
58
|
-
raw: string | undefined;
|
|
59
|
-
} {
|
|
60
|
-
const text = typeof input === "string" ? input : "";
|
|
61
|
-
if (text === "stop" || text === "length" || text === "content-filter" || text === "tool-calls" || text === "error") {
|
|
62
|
-
return { unified: text, raw: text };
|
|
63
|
-
}
|
|
64
|
-
return { unified: "stop", raw: text || "stop" };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function stringifyToolInput(input: unknown): string {
|
|
68
|
-
if (typeof input === "string") return input;
|
|
69
|
-
try {
|
|
70
|
-
return JSON.stringify(input ?? {});
|
|
71
|
-
} catch {
|
|
72
|
-
return "{}";
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function textFromProviderContent(content: unknown): string {
|
|
77
|
-
if (typeof content === "string") return content;
|
|
78
|
-
if (!Array.isArray(content)) return "";
|
|
79
|
-
return content
|
|
80
|
-
.filter((part) => part && typeof part === "object" && (part as { type?: unknown }).type === "text")
|
|
81
|
-
.map((part) => String((part as { text?: unknown }).text ?? ""))
|
|
82
|
-
.join("\n")
|
|
83
|
-
.trim();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function fileUrlFromProviderPart(part: Record<string, unknown>): string {
|
|
87
|
-
const data = part.data;
|
|
88
|
-
if (data instanceof URL) return data.toString();
|
|
89
|
-
if (typeof data === "string") return data;
|
|
90
|
-
return "";
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function resolveProviderPromptRole(input: unknown): ProviderPromptRole {
|
|
94
|
-
return input === "system" || input === "user" || input === "assistant" || input === "tool"
|
|
95
|
-
? input
|
|
96
|
-
: "user";
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function normalizeProviderToolResultOutput(
|
|
100
|
-
output: ProviderToolResultOutput,
|
|
101
|
-
): {
|
|
102
|
-
/**
|
|
103
|
-
* 归一后的 tool part 状态。
|
|
104
|
-
*/
|
|
105
|
-
state: "output-available" | "output-error";
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* tool 成功输出。
|
|
109
|
-
*/
|
|
110
|
-
output?: unknown;
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* tool 错误文本。
|
|
114
|
-
*/
|
|
115
|
-
errorText?: string;
|
|
116
|
-
} {
|
|
117
|
-
const outputType = typeof output.type === "string" ? output.type : "";
|
|
118
|
-
if (outputType === "json" || outputType === "text" || outputType === "content") {
|
|
119
|
-
return {
|
|
120
|
-
state: "output-available",
|
|
121
|
-
output: "value" in output ? output.value : null,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
if (outputType === "error-json") {
|
|
125
|
-
return {
|
|
126
|
-
state: "output-error",
|
|
127
|
-
errorText: stringifyToolInput("value" in output ? output.value : null),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
if (outputType === "error-text") {
|
|
131
|
-
return {
|
|
132
|
-
state: "output-error",
|
|
133
|
-
errorText: String(output.value ?? ""),
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
if (outputType === "execution-denied") {
|
|
137
|
-
return {
|
|
138
|
-
state: "output-error",
|
|
139
|
-
errorText: String(output.reason ?? "tool execution denied"),
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
return {
|
|
143
|
-
state: "output-available",
|
|
144
|
-
output,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function providerContentToUiParts(
|
|
149
|
-
content: unknown,
|
|
150
|
-
existingParts?: UIMessage["parts"],
|
|
151
|
-
): UIMessage["parts"] {
|
|
152
|
-
if (!Array.isArray(content)) {
|
|
153
|
-
return [{ type: "text", text: textFromProviderContent(content) }];
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const parts: UIMessage["parts"] = Array.isArray(existingParts)
|
|
157
|
-
? [...existingParts]
|
|
158
|
-
: [];
|
|
159
|
-
for (const part of content) {
|
|
160
|
-
if (!part || typeof part !== "object") continue;
|
|
161
|
-
const record = part as Record<string, unknown>;
|
|
162
|
-
if (record.type === "text") {
|
|
163
|
-
parts.push({ type: "text", text: String(record.text ?? "") });
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
if (record.type === "reasoning") {
|
|
167
|
-
parts.push({ type: "reasoning", text: String(record.text ?? "") });
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
if (record.type === "file") {
|
|
171
|
-
const url = fileUrlFromProviderPart(record);
|
|
172
|
-
if (!url) continue;
|
|
173
|
-
parts.push({
|
|
174
|
-
type: "file",
|
|
175
|
-
mediaType: String(record.mediaType ?? "application/octet-stream"),
|
|
176
|
-
filename: typeof record.filename === "string" ? record.filename : undefined,
|
|
177
|
-
url,
|
|
178
|
-
});
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
if (record.type === "tool-call") {
|
|
182
|
-
parts.push({
|
|
183
|
-
type: "dynamic-tool",
|
|
184
|
-
toolName: String(record.toolName ?? ""),
|
|
185
|
-
toolCallId: String(record.toolCallId ?? ""),
|
|
186
|
-
state: "input-available",
|
|
187
|
-
input: record.input,
|
|
188
|
-
providerExecuted: Boolean(record.providerExecuted),
|
|
189
|
-
});
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (record.type === "tool-result") {
|
|
193
|
-
const toolCallId = String(record.toolCallId ?? "");
|
|
194
|
-
const toolName = String(record.toolName ?? "");
|
|
195
|
-
const normalizedOutput = normalizeProviderToolResultOutput(
|
|
196
|
-
(record.output as ProviderToolResultOutput | undefined) ?? {},
|
|
197
|
-
);
|
|
198
|
-
const existingPart = parts.find((item) => {
|
|
199
|
-
if (!item || typeof item !== "object") return false;
|
|
200
|
-
const toolPart = item as { toolCallId?: unknown };
|
|
201
|
-
return String(toolPart.toolCallId ?? "") === toolCallId;
|
|
202
|
-
}) as
|
|
203
|
-
| ({
|
|
204
|
-
toolCallId?: unknown;
|
|
205
|
-
toolName?: unknown;
|
|
206
|
-
input?: unknown;
|
|
207
|
-
providerExecuted?: unknown;
|
|
208
|
-
} & Record<string, unknown>)
|
|
209
|
-
| undefined;
|
|
210
|
-
const baseInput = existingPart?.input ?? null;
|
|
211
|
-
const baseToolName = String(existingPart?.toolName ?? toolName);
|
|
212
|
-
const nextPart = normalizedOutput.state === "output-available"
|
|
213
|
-
? {
|
|
214
|
-
type: "dynamic-tool" as const,
|
|
215
|
-
toolName: baseToolName,
|
|
216
|
-
toolCallId,
|
|
217
|
-
state: "output-available" as const,
|
|
218
|
-
input: baseInput,
|
|
219
|
-
output: normalizedOutput.output,
|
|
220
|
-
providerExecuted: false,
|
|
221
|
-
}
|
|
222
|
-
: {
|
|
223
|
-
type: "dynamic-tool" as const,
|
|
224
|
-
toolName: baseToolName,
|
|
225
|
-
toolCallId,
|
|
226
|
-
state: "output-error" as const,
|
|
227
|
-
input: baseInput,
|
|
228
|
-
errorText: normalizedOutput.errorText ?? "tool_error",
|
|
229
|
-
providerExecuted: false,
|
|
230
|
-
};
|
|
231
|
-
if (existingPart) {
|
|
232
|
-
const index = parts.indexOf(existingPart as never);
|
|
233
|
-
if (index >= 0) {
|
|
234
|
-
parts[index] = nextPart;
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
parts.push(nextPart);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return parts;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function providerPromptToMessages(prompt: unknown): UIMessage[] {
|
|
245
|
-
if (!Array.isArray(prompt)) return [];
|
|
246
|
-
const messages: UIMessage[] = [];
|
|
247
|
-
for (const [index, message] of prompt.entries()) {
|
|
248
|
-
if (!message || typeof message !== "object") continue;
|
|
249
|
-
const item = message as ProviderPromptMessage;
|
|
250
|
-
const role = resolveProviderPromptRole(item.role);
|
|
251
|
-
if (role === "tool") {
|
|
252
|
-
const lastAssistantMessage = [...messages]
|
|
253
|
-
.reverse()
|
|
254
|
-
.find((candidate) => candidate.role === "assistant");
|
|
255
|
-
if (lastAssistantMessage) {
|
|
256
|
-
lastAssistantMessage.parts = providerContentToUiParts(
|
|
257
|
-
item.content,
|
|
258
|
-
lastAssistantMessage.parts,
|
|
259
|
-
);
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
messages.push({
|
|
263
|
-
id: `city-model-message-${String(index)}`,
|
|
264
|
-
role: "assistant",
|
|
265
|
-
parts: providerContentToUiParts(item.content),
|
|
266
|
-
});
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
messages.push({
|
|
270
|
-
id: `city-model-message-${String(index)}`,
|
|
271
|
-
role,
|
|
272
|
-
parts: providerContentToUiParts(item.content),
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
return messages;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function providerOptionsToInput(options: Record<string, unknown>): CityModelInvokeInput {
|
|
279
|
-
return {
|
|
280
|
-
messages: providerPromptToMessages(options.prompt),
|
|
281
|
-
tools: options.tools,
|
|
282
|
-
toolChoice: options.toolChoice,
|
|
283
|
-
providerOptions: options.providerOptions,
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function textFromUiMessage(message: UIMessage): string {
|
|
288
|
-
return message.parts
|
|
289
|
-
.filter((part) => part.type === "text")
|
|
290
|
-
.map((part) => String((part as { text?: unknown }).text ?? ""))
|
|
291
|
-
.join("\n")
|
|
292
|
-
.trim();
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function uiMessageToProviderContent(message: UIMessage): ProviderContentPart[] {
|
|
296
|
-
return message.parts.flatMap((part): ProviderContentPart[] => {
|
|
297
|
-
if (part.type === "text") {
|
|
298
|
-
return [{ type: "text", text: String((part as { text?: unknown }).text ?? "") }];
|
|
299
|
-
}
|
|
300
|
-
if (part.type === "reasoning") {
|
|
301
|
-
return [{ type: "reasoning", text: String((part as { text?: unknown }).text ?? "") }];
|
|
302
|
-
}
|
|
303
|
-
if (part.type === "dynamic-tool") {
|
|
304
|
-
const toolPart = part as {
|
|
305
|
-
toolCallId?: unknown;
|
|
306
|
-
toolName?: unknown;
|
|
307
|
-
input?: unknown;
|
|
308
|
-
output?: unknown;
|
|
309
|
-
errorText?: unknown;
|
|
310
|
-
state?: unknown;
|
|
311
|
-
providerExecuted?: unknown;
|
|
312
|
-
};
|
|
313
|
-
const content: ProviderContentPart[] = [{
|
|
314
|
-
type: "tool-call",
|
|
315
|
-
toolCallId: String(toolPart.toolCallId ?? ""),
|
|
316
|
-
toolName: String(toolPart.toolName ?? ""),
|
|
317
|
-
input: stringifyToolInput(toolPart.input),
|
|
318
|
-
providerExecuted: Boolean(toolPart.providerExecuted),
|
|
319
|
-
}];
|
|
320
|
-
if (toolPart.state === "output-available") {
|
|
321
|
-
content.push({
|
|
322
|
-
type: "tool-result",
|
|
323
|
-
toolCallId: String(toolPart.toolCallId ?? ""),
|
|
324
|
-
toolName: String(toolPart.toolName ?? ""),
|
|
325
|
-
output: {
|
|
326
|
-
type: "json",
|
|
327
|
-
value: toolPart.output ?? null,
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
|
-
} else if (toolPart.state === "output-error") {
|
|
331
|
-
content.push({
|
|
332
|
-
type: "tool-result",
|
|
333
|
-
toolCallId: String(toolPart.toolCallId ?? ""),
|
|
334
|
-
toolName: String(toolPart.toolName ?? ""),
|
|
335
|
-
output: {
|
|
336
|
-
type: "error-text",
|
|
337
|
-
value: String(toolPart.errorText ?? "tool_error"),
|
|
338
|
-
},
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
return content;
|
|
342
|
-
}
|
|
343
|
-
return [];
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function enqueueFinish(
|
|
348
|
-
controller: ProviderStreamController,
|
|
349
|
-
finishReason: unknown,
|
|
350
|
-
): void {
|
|
351
|
-
controller.enqueue({
|
|
352
|
-
type: "finish",
|
|
353
|
-
finishReason: normalizeFinishReason(finishReason),
|
|
354
|
-
usage: {
|
|
355
|
-
inputTokens: {
|
|
356
|
-
total: undefined,
|
|
357
|
-
noCache: undefined,
|
|
358
|
-
cacheRead: undefined,
|
|
359
|
-
cacheWrite: undefined,
|
|
360
|
-
},
|
|
361
|
-
outputTokens: {
|
|
362
|
-
total: undefined,
|
|
363
|
-
text: undefined,
|
|
364
|
-
reasoning: undefined,
|
|
365
|
-
},
|
|
366
|
-
},
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function enqueueProviderParts(
|
|
371
|
-
controller: ProviderStreamController,
|
|
372
|
-
parts: Record<string, unknown>[],
|
|
373
|
-
state: {
|
|
374
|
-
/**
|
|
375
|
-
* 当前流是否已经发出 stream-start。
|
|
376
|
-
*/
|
|
377
|
-
sawStart: boolean;
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* 当前流是否已经发出 finish。
|
|
381
|
-
*/
|
|
382
|
-
sawFinish: boolean;
|
|
383
|
-
},
|
|
384
|
-
): void {
|
|
385
|
-
for (const part of parts) {
|
|
386
|
-
if (part.type !== "stream-start" && !state.sawStart) {
|
|
387
|
-
controller.enqueue({ type: "stream-start", warnings: [] });
|
|
388
|
-
state.sawStart = true;
|
|
389
|
-
}
|
|
390
|
-
if (part.type === "stream-start") state.sawStart = true;
|
|
391
|
-
if (part.type === "finish") state.sawFinish = true;
|
|
392
|
-
controller.enqueue(part);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function mapUiChunkToProviderParts(chunk: UIMessageChunk): ProviderContentPart[] {
|
|
397
|
-
switch (chunk.type) {
|
|
398
|
-
case "start":
|
|
399
|
-
return [{ type: "stream-start", warnings: [] }];
|
|
400
|
-
case "text-start":
|
|
401
|
-
return [{ type: "text-start", id: chunk.id }];
|
|
402
|
-
case "text-delta":
|
|
403
|
-
return [{ type: "text-delta", id: chunk.id, delta: chunk.delta }];
|
|
404
|
-
case "text-end":
|
|
405
|
-
return [{ type: "text-end", id: chunk.id }];
|
|
406
|
-
case "reasoning-start":
|
|
407
|
-
return [{ type: "reasoning-start", id: chunk.id }];
|
|
408
|
-
case "reasoning-delta":
|
|
409
|
-
return [{ type: "reasoning-delta", id: chunk.id, delta: chunk.delta }];
|
|
410
|
-
case "reasoning-end":
|
|
411
|
-
return [{ type: "reasoning-end", id: chunk.id }];
|
|
412
|
-
case "tool-input-start":
|
|
413
|
-
return [{
|
|
414
|
-
type: "tool-input-start",
|
|
415
|
-
id: chunk.toolCallId,
|
|
416
|
-
toolName: chunk.toolName,
|
|
417
|
-
providerExecuted: chunk.providerExecuted,
|
|
418
|
-
dynamic: chunk.dynamic,
|
|
419
|
-
}];
|
|
420
|
-
case "tool-input-delta":
|
|
421
|
-
return [{
|
|
422
|
-
type: "tool-input-delta",
|
|
423
|
-
id: chunk.toolCallId,
|
|
424
|
-
delta: chunk.inputTextDelta,
|
|
425
|
-
}];
|
|
426
|
-
case "tool-input-available":
|
|
427
|
-
return [{
|
|
428
|
-
type: "tool-call",
|
|
429
|
-
toolCallId: chunk.toolCallId,
|
|
430
|
-
toolName: chunk.toolName,
|
|
431
|
-
input: stringifyToolInput(chunk.input),
|
|
432
|
-
providerExecuted: chunk.providerExecuted,
|
|
433
|
-
dynamic: chunk.dynamic,
|
|
434
|
-
}];
|
|
435
|
-
case "tool-output-available":
|
|
436
|
-
return [{
|
|
437
|
-
type: "tool-result",
|
|
438
|
-
toolCallId: chunk.toolCallId,
|
|
439
|
-
toolName: "",
|
|
440
|
-
output: { type: "json", value: chunk.output },
|
|
441
|
-
providerExecuted: chunk.providerExecuted,
|
|
442
|
-
dynamic: chunk.dynamic,
|
|
443
|
-
}];
|
|
444
|
-
case "error":
|
|
445
|
-
return [{ type: "error", error: new Error(chunk.errorText) }];
|
|
446
|
-
default:
|
|
447
|
-
return [];
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
24
|
+
/**
|
|
25
|
+
* 将 CityModel 的 hidden connection 转换为 AI SDK LanguageModel。
|
|
26
|
+
*/
|
|
451
27
|
function cityModelToLanguageModel(model: CityModel): LanguageModel {
|
|
452
|
-
const
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
noCache: undefined,
|
|
467
|
-
cacheRead: undefined,
|
|
468
|
-
cacheWrite: undefined,
|
|
469
|
-
},
|
|
470
|
-
outputTokens: {
|
|
471
|
-
total: undefined,
|
|
472
|
-
text: undefined,
|
|
473
|
-
reasoning: undefined,
|
|
474
|
-
},
|
|
475
|
-
},
|
|
476
|
-
response: {
|
|
477
|
-
modelId: model.id,
|
|
478
|
-
},
|
|
479
|
-
warnings: [],
|
|
480
|
-
};
|
|
481
|
-
},
|
|
482
|
-
async doStream(options) {
|
|
483
|
-
const cityStream = await invoker.stream(providerOptionsToInput(options as Record<string, unknown>));
|
|
484
|
-
return {
|
|
485
|
-
stream: new ReadableStream({
|
|
486
|
-
async start(controller: ProviderStreamController) {
|
|
487
|
-
const reader = cityStream.getReader();
|
|
488
|
-
const state = {
|
|
489
|
-
sawStart: false,
|
|
490
|
-
sawFinish: false,
|
|
491
|
-
};
|
|
492
|
-
try {
|
|
493
|
-
while (true) {
|
|
494
|
-
const { done, value } = await reader.read();
|
|
495
|
-
if (done) break;
|
|
496
|
-
const parts = mapUiChunkToProviderParts(value);
|
|
497
|
-
enqueueProviderParts(controller, parts, state);
|
|
498
|
-
}
|
|
499
|
-
if (!state.sawStart) controller.enqueue({ type: "stream-start", warnings: [] });
|
|
500
|
-
if (!state.sawFinish) enqueueFinish(controller, "stop");
|
|
501
|
-
controller.close();
|
|
502
|
-
} catch (error) {
|
|
503
|
-
controller.enqueue({ type: "error", error });
|
|
504
|
-
if (!state.sawFinish) enqueueFinish(controller, "error");
|
|
505
|
-
controller.close();
|
|
506
|
-
} finally {
|
|
507
|
-
reader.releaseLock();
|
|
508
|
-
}
|
|
509
|
-
},
|
|
510
|
-
}),
|
|
511
|
-
response: {
|
|
512
|
-
modelId: model.id,
|
|
513
|
-
},
|
|
514
|
-
};
|
|
515
|
-
},
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
return languageModel as unknown as LanguageModel;
|
|
28
|
+
const connection = model[CITY_MODEL_INVOKER].connection();
|
|
29
|
+
const provider = createOpenAICompatible({
|
|
30
|
+
name: "downcity",
|
|
31
|
+
baseURL: connection.base_url,
|
|
32
|
+
apiKey: connection.api_key,
|
|
33
|
+
transformRequestBody: (body) => ({
|
|
34
|
+
...body,
|
|
35
|
+
...connection.request_body,
|
|
36
|
+
model: typeof body.model === "string" && body.model.trim()
|
|
37
|
+
? body.model
|
|
38
|
+
: connection.model_id,
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
return provider.languageModel(connection.model_id) as LanguageModel;
|
|
519
42
|
}
|
|
520
43
|
|
|
521
44
|
/**
|
|
522
45
|
* 将 Agent 可接受的模型输入归一为 AI SDK LanguageModel。
|
|
523
46
|
*/
|
|
524
47
|
export function normalizeAgentModel(model: AgentModel): LanguageModel {
|
|
525
|
-
if (isCityModel(model))
|
|
48
|
+
if (isCityModel(model)) {
|
|
49
|
+
return cityModelToLanguageModel(model);
|
|
50
|
+
}
|
|
526
51
|
return model;
|
|
527
52
|
}
|
|
528
53
|
|