@armadra/agent 0.3.0 → 0.4.0
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/CHANGELOG.md +84 -0
- package/README.md +93 -44
- package/dist/agent/retry.d.ts +1 -1
- package/dist/agent/retry.js +2 -1
- package/dist/agent/session-cache.js +1 -1
- package/dist/agent/session-classifier.d.ts +18 -0
- package/dist/agent/session-classifier.js +103 -0
- package/dist/agent/session-core.d.ts +13 -0
- package/dist/agent/session-tools.js +24 -3
- package/dist/agent/session.d.ts +3 -0
- package/dist/agent/session.js +12 -3
- package/dist/agent/tool-runner.js +15 -2
- package/dist/agent/types.d.ts +9 -1
- package/dist/ai/apis/anthropic-messages.js +3 -2
- package/dist/ai/apis/google-generative-ai.js +3 -2
- package/dist/ai/apis/openai-completions.js +3 -2
- package/dist/ai/apis/openai-responses.js +3 -2
- package/dist/ai/http.d.ts +28 -6
- package/dist/ai/http.js +41 -8
- package/dist/ai/providers/registry.d.ts +8 -1
- package/dist/ai/providers/registry.js +30 -22
- package/dist/ai/providers/suggest.d.ts +18 -0
- package/dist/ai/providers/suggest.js +72 -0
- package/dist/ai/sse.d.ts +6 -2
- package/dist/ai/sse.js +22 -2
- package/dist/ai/types.d.ts +9 -2
- package/dist/bundle/ama.cjs +15279 -10151
- package/dist/cli/args.d.ts +19 -4
- package/dist/cli/args.js +83 -10
- package/dist/cli/bootstrap.js +27 -3
- package/dist/cli/codemode-notice.d.ts +20 -0
- package/dist/cli/codemode-notice.js +55 -0
- package/dist/cli/compose-session.d.ts +5 -0
- package/dist/cli/compose-session.js +32 -1
- package/dist/cli/compose-store.d.ts +1 -1
- package/dist/cli/compose-store.js +3 -1
- package/dist/cli/compose.d.ts +14 -4
- package/dist/cli/compose.js +29 -10
- package/dist/cli/default-model.d.ts +38 -1
- package/dist/cli/default-model.js +96 -9
- package/dist/cli/deps.d.ts +36 -0
- package/dist/cli/exit-codes.d.ts +2 -0
- package/dist/cli/exit-codes.js +3 -0
- package/dist/cli/fake-visibility.d.ts +13 -0
- package/dist/cli/fake-visibility.js +26 -0
- package/dist/cli/from-prompt.d.ts +18 -0
- package/dist/cli/from-prompt.js +49 -0
- package/dist/cli/main.d.ts +9 -2
- package/dist/cli/main.js +77 -3
- package/dist/cli/proxy.d.ts +51 -0
- package/dist/cli/proxy.js +135 -0
- package/dist/cli/startup-screen.d.ts +29 -0
- package/dist/cli/startup-screen.js +47 -0
- package/dist/cli/startup-steps.d.ts +1 -1
- package/dist/cli/startup-steps.js +14 -12
- package/dist/cli/subcommands/config.d.ts +19 -3
- package/dist/cli/subcommands/config.js +108 -21
- package/dist/cli/subcommands/context.js +4 -1
- package/dist/cli/subcommands/doctor.js +12 -1
- package/dist/cli/subcommands/init.js +2 -1
- package/dist/cli/subcommands/models-discover.d.ts +15 -9
- package/dist/cli/subcommands/models-discover.js +53 -46
- package/dist/cli/subcommands/probe-runner.d.ts +96 -0
- package/dist/cli/subcommands/probe-runner.js +264 -0
- package/dist/cli/subcommands/providers-probe.d.ts +34 -0
- package/dist/cli/subcommands/providers-probe.js +87 -0
- package/dist/cli/subcommands/providers.d.ts +3 -2
- package/dist/cli/subcommands/providers.js +48 -45
- package/dist/cli/subcommands/sessions-export.d.ts +10 -0
- package/dist/cli/subcommands/sessions-export.js +59 -0
- package/dist/cli/subcommands/sessions-search.d.ts +13 -0
- package/dist/cli/subcommands/sessions-search.js +103 -0
- package/dist/cli/subcommands/sessions.d.ts +3 -2
- package/dist/cli/subcommands/sessions.js +22 -1
- package/dist/cli/subcommands/stats.d.ts +17 -0
- package/dist/cli/subcommands/stats.js +198 -0
- package/dist/cli/system-prompt-arg.d.ts +11 -0
- package/dist/cli/system-prompt-arg.js +34 -0
- package/dist/codemode/modes.d.ts +4 -11
- package/dist/codemode/modes.js +5 -27
- package/dist/codemode/tool.d.ts +18 -14
- package/dist/codemode/tool.js +71 -28
- package/dist/config/init.d.ts +6 -2
- package/dist/config/init.js +12 -5
- package/dist/config/json-schema.d.ts +1 -0
- package/dist/config/json-schema.js +36 -3
- package/dist/config/key-docs.d.ts +22 -0
- package/dist/config/key-docs.js +105 -0
- package/dist/config/merge.d.ts +9 -8
- package/dist/config/merge.js +22 -7
- package/dist/config/schema.d.ts +1 -1
- package/dist/config/schema.js +24 -5
- package/dist/config/types.d.ts +34 -6
- package/dist/config/types.js +17 -1
- package/dist/modes/commands-core.js +6 -5
- package/dist/modes/interactive/approval-dialog.d.ts +21 -5
- package/dist/modes/interactive/approval-dialog.js +106 -27
- package/dist/modes/interactive/commands.d.ts +11 -2
- package/dist/modes/interactive/commands.js +56 -16
- package/dist/modes/interactive/interactive-mode.d.ts +2 -1
- package/dist/modes/interactive/interactive-mode.js +65 -81
- package/dist/modes/interactive/key-dispatch.d.ts +3 -1
- package/dist/modes/interactive/key-dispatch.js +5 -6
- package/dist/modes/interactive/line/line-mode.d.ts +1 -0
- package/dist/modes/interactive/line/line-mode.js +6 -4
- package/dist/modes/interactive/line/line-render.d.ts +4 -0
- package/dist/modes/interactive/line/line-render.js +28 -6
- package/dist/modes/interactive/message-view.d.ts +48 -9
- package/dist/modes/interactive/message-view.js +238 -44
- package/dist/modes/interactive/panels.d.ts +18 -0
- package/dist/modes/interactive/panels.js +143 -0
- package/dist/modes/interactive/pickers.d.ts +23 -2
- package/dist/modes/interactive/pickers.js +48 -15
- package/dist/modes/interactive/run-indicator.d.ts +51 -0
- package/dist/modes/interactive/run-indicator.js +189 -0
- package/dist/modes/interactive/startup-header.d.ts +40 -0
- package/dist/modes/interactive/startup-header.js +169 -0
- package/dist/modes/interactive/status-bar.d.ts +18 -15
- package/dist/modes/interactive/status-bar.js +98 -56
- package/dist/modes/interactive/tool-summary.d.ts +46 -0
- package/dist/modes/interactive/tool-summary.js +218 -0
- package/dist/modes/interactive/tool-view.d.ts +48 -15
- package/dist/modes/interactive/tool-view.js +203 -145
- package/dist/modes/print/print-mode.d.ts +31 -4
- package/dist/modes/print/print-mode.js +116 -7
- package/dist/modes/rpc/commands.js +5 -0
- package/dist/permissions/auto-safe.d.ts +60 -0
- package/dist/permissions/auto-safe.js +529 -0
- package/dist/permissions/classifier.d.ts +64 -0
- package/dist/permissions/classifier.js +184 -0
- package/dist/permissions/dangerous.d.ts +5 -0
- package/dist/permissions/dangerous.js +1 -1
- package/dist/permissions/modes.d.ts +30 -0
- package/dist/permissions/modes.js +78 -0
- package/dist/permissions/pipeline.d.ts +31 -4
- package/dist/permissions/pipeline.js +196 -6
- package/dist/permissions/protected.d.ts +19 -0
- package/dist/permissions/protected.js +74 -0
- package/dist/permissions/rules.js +3 -0
- package/dist/permissions/types.d.ts +50 -3
- package/dist/permissions/types.js +3 -0
- package/dist/sdk.d.ts +9 -3
- package/dist/sdk.js +10 -2
- package/dist/session/export.d.ts +32 -0
- package/dist/session/export.js +187 -0
- package/dist/session/redact.d.ts +15 -0
- package/dist/session/redact.js +55 -0
- package/dist/session/reuse.d.ts +33 -0
- package/dist/session/reuse.js +86 -0
- package/dist/session/scan.d.ts +34 -0
- package/dist/session/scan.js +140 -0
- package/dist/session/search.d.ts +52 -0
- package/dist/session/search.js +211 -0
- package/dist/session/stats-aggregate.d.ts +63 -0
- package/dist/session/stats-aggregate.js +163 -0
- package/dist/session/stats-index.d.ts +26 -0
- package/dist/session/stats-index.js +91 -0
- package/dist/session/stats-scan.d.ts +54 -0
- package/dist/session/stats-scan.js +236 -0
- package/dist/tools/presets.d.ts +35 -8
- package/dist/tools/presets.js +56 -17
- package/dist/tui/component.d.ts +8 -2
- package/dist/tui/component.js +3 -1
- package/dist/tui/components/box.d.ts +6 -1
- package/dist/tui/components/box.js +16 -6
- package/dist/tui/components/card.d.ts +23 -0
- package/dist/tui/components/card.js +37 -0
- package/dist/tui/components/editor-history.d.ts +6 -0
- package/dist/tui/components/editor-history.js +45 -0
- package/dist/tui/components/editor-paste.d.ts +1 -1
- package/dist/tui/components/editor-paste.js +4 -4
- package/dist/tui/components/editor.d.ts +11 -5
- package/dist/tui/components/editor.js +52 -58
- package/dist/tui/components/key-value.d.ts +3 -0
- package/dist/tui/components/key-value.js +16 -6
- package/dist/tui/components/loader.d.ts +37 -7
- package/dist/tui/components/loader.js +84 -21
- package/dist/tui/components/markdown.d.ts +5 -1
- package/dist/tui/components/markdown.js +45 -21
- package/dist/tui/components/meter.d.ts +3 -3
- package/dist/tui/components/meter.js +13 -11
- package/dist/tui/components/select-list.d.ts +23 -1
- package/dist/tui/components/select-list.js +76 -13
- package/dist/tui/glyphs.d.ts +68 -0
- package/dist/tui/glyphs.js +114 -0
- package/dist/tui/theme.d.ts +30 -6
- package/dist/tui/theme.js +103 -17
- package/dist/tui.d.ts +4 -2
- package/dist/tui.js +3 -1
- package/docs/codemode.md +23 -9
- package/docs/hooks.md +10 -10
- package/docs/permissions.md +148 -0
- package/docs/providers.md +33 -10
- package/docs/rpc.md +36 -36
- package/docs/session-format.md +2 -1
- package/docs/sessions.md +134 -0
- package/docs/tui.md +139 -62
- package/package.json +3 -1
package/dist/agent/session.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { Model, ModelThinkingLevel } from "../ai/types.js";
|
|
14
14
|
import type { HookEvent, HookEventPayload, HookOutcome } from "../hooks/types.js";
|
|
15
|
+
import type { PermissionClassifier } from "../permissions/classifier.js";
|
|
15
16
|
import type { PermissionMode } from "../permissions/types.js";
|
|
16
17
|
import type { SessionManager } from "../session/manager.js";
|
|
17
18
|
import type { AgentMessage, BranchSummaryEntry, SessionEntry, SessionEntryInput } from "../session/types.js";
|
|
@@ -47,6 +48,7 @@ export declare class AgentSessionImpl implements AgentSession, SessionCore {
|
|
|
47
48
|
private systemInput;
|
|
48
49
|
private readonly subagentPool;
|
|
49
50
|
private disposed;
|
|
51
|
+
private classifier;
|
|
50
52
|
/** 已入队、尚未投递的消息(投递时发 queue_update)。 */
|
|
51
53
|
private readonly queuedMessages;
|
|
52
54
|
constructor(options: AgentSessionOptions);
|
|
@@ -113,5 +115,6 @@ export declare class AgentSessionImpl implements AgentSession, SessionCore {
|
|
|
113
115
|
getStats(): SessionStats;
|
|
114
116
|
dispose(): Promise<void>;
|
|
115
117
|
childBase(): Pick<AgentSessionOptions, "model" | "thinkingLevel" | "activeTools" | "system">;
|
|
118
|
+
autoClassifier(): PermissionClassifier;
|
|
116
119
|
spawnSubagent(request: SubagentRequest): Promise<SubagentResult>;
|
|
117
120
|
}
|
package/dist/agent/session.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { modelRefOf } from "../ai/providers/channels.js";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { AmaError } from "../errors.js";
|
|
16
|
+
import { createSessionClassifier } from "./session-classifier.js";
|
|
16
17
|
import { buildProjection } from "../session/projection.js";
|
|
17
18
|
import { Agent } from "./agent.js";
|
|
18
19
|
import { queuedText } from "./queue.js";
|
|
@@ -53,6 +54,7 @@ export class AgentSessionImpl {
|
|
|
53
54
|
systemInput;
|
|
54
55
|
subagentPool;
|
|
55
56
|
disposed = false;
|
|
57
|
+
classifier;
|
|
56
58
|
/** 已入队、尚未投递的消息(投递时发 queue_update)。 */
|
|
57
59
|
queuedMessages = new WeakSet();
|
|
58
60
|
constructor(options) {
|
|
@@ -103,9 +105,12 @@ export class AgentSessionImpl {
|
|
|
103
105
|
getThinkingLevel: () => this.currentThinking,
|
|
104
106
|
streamOptions: async () => {
|
|
105
107
|
const apiKey = await this.resolveApiKey();
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
const idle = options.idleTimeoutMs;
|
|
109
|
+
return {
|
|
110
|
+
sessionId: this.manager.id,
|
|
111
|
+
...(apiKey === undefined ? {} : { apiKey }),
|
|
112
|
+
...(idle === undefined ? {} : { idleTimeoutMs: idle }),
|
|
113
|
+
};
|
|
109
114
|
},
|
|
110
115
|
beforeRequest: async () => {
|
|
111
116
|
this.syncSystem();
|
|
@@ -479,6 +484,10 @@ export class AgentSessionImpl {
|
|
|
479
484
|
system: this.systemInput,
|
|
480
485
|
};
|
|
481
486
|
}
|
|
487
|
+
autoClassifier() {
|
|
488
|
+
this.classifier ??= createSessionClassifier(this);
|
|
489
|
+
return this.classifier;
|
|
490
|
+
}
|
|
482
491
|
spawnSubagent(request) {
|
|
483
492
|
return runSubagent(this, request, this.subagentPool, (options) => new AgentSessionImpl(options));
|
|
484
493
|
}
|
|
@@ -25,6 +25,10 @@ export const DEFAULT_MAX_TOOL_RESULT_CHARS = 30_000;
|
|
|
25
25
|
export const DEFAULT_ABORT_GRACE_MS = 3000;
|
|
26
26
|
/** 嵌套调用结果的截断上限(给脚本,不进上下文)。 */
|
|
27
27
|
export const NESTED_MAX_RESULT_CHARS = 1024 * 1024;
|
|
28
|
+
/** 门禁给出的 auto 判定,随 tool_execution_end 发出(按调用对象记,调用结束后随之回收)。 */
|
|
29
|
+
const autoDecisions = new WeakMap();
|
|
30
|
+
/** 门禁拒绝(gate.block)而没有执行的调用:tool_execution_end 带 `denied`。 */
|
|
31
|
+
const deniedCalls = new WeakSet();
|
|
28
32
|
export function errorResult(text) {
|
|
29
33
|
return { content: text, isError: true };
|
|
30
34
|
}
|
|
@@ -80,9 +84,12 @@ async function prepare(call, assistant, options, signal, parent) {
|
|
|
80
84
|
try {
|
|
81
85
|
const gateContext = parent === undefined ? { signal, assistant, tool } : { signal, assistant, tool, parent };
|
|
82
86
|
const gate = await options.beforeToolCall(call, gateContext);
|
|
87
|
+
if (gate.autoDecision !== undefined)
|
|
88
|
+
autoDecisions.set(call, gate.autoDecision);
|
|
83
89
|
if (signal.aborted)
|
|
84
90
|
return { kind: "immediate", call, result: errorResult(ABORTED_TOOL_TEXT) };
|
|
85
91
|
if (gate.block === true) {
|
|
92
|
+
deniedCalls.add(call);
|
|
86
93
|
return {
|
|
87
94
|
kind: "immediate",
|
|
88
95
|
call,
|
|
@@ -193,13 +200,19 @@ async function execute(prepared, options, signal, emit, parent) {
|
|
|
193
200
|
return truncateResult(call, result, options, parent !== undefined);
|
|
194
201
|
}
|
|
195
202
|
async function emitEnd(call, result, emit) {
|
|
196
|
-
|
|
203
|
+
const event = {
|
|
197
204
|
type: "tool_execution_end",
|
|
198
205
|
toolCallId: call.id,
|
|
199
206
|
toolName: call.name,
|
|
200
207
|
result,
|
|
201
208
|
isError: result.isError === true,
|
|
202
|
-
}
|
|
209
|
+
};
|
|
210
|
+
const auto = autoDecisions.get(call);
|
|
211
|
+
if (auto !== undefined)
|
|
212
|
+
event.autoDecision = auto;
|
|
213
|
+
if (deniedCalls.has(call))
|
|
214
|
+
event.denied = true;
|
|
215
|
+
await emit(event);
|
|
203
216
|
}
|
|
204
217
|
async function emitResultMessages(finalized, emit) {
|
|
205
218
|
const messages = [];
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import type { AssistantEvent, AssistantMessage, CacheRetention, ImageBlock, Message, MessageOrigin, Model, ModelRef, ModelThinkingLevel, ToolCallBlock, ToolResultMessage, TranscriptContext, Usage } from "../ai/types.js";
|
|
22
22
|
import type { CacheMiss, CacheMissReason, CacheReporting, WarmerStatus, WarmingMode } from "../ai/cache/types.js";
|
|
23
23
|
import type { HookEvent } from "../hooks/types.js";
|
|
24
|
-
import type { ActionPreview, ApprovalDecision, ApprovalReason, PermissionMode } from "../permissions/types.js";
|
|
24
|
+
import type { ActionPreview, ApprovalDecision, ApprovalReason, AutoDecision, PermissionMode } from "../permissions/types.js";
|
|
25
25
|
import type { AgentMessage, SessionEntry } from "../session/types.js";
|
|
26
26
|
import type { ToolDefinition, ToolResult } from "../tools/types.js";
|
|
27
27
|
export type { AssistantContentBlock, AssistantMessage, ContentBlock, ImageBlock, Message, StopReason, SystemMessage, TextBlock, ThinkingBlock, ToolCallBlock, ToolResultMessage, Usage, UserMessage, } from "../ai/types.js";
|
|
@@ -56,6 +56,8 @@ export interface ToolCallGate {
|
|
|
56
56
|
reason?: string;
|
|
57
57
|
/** Hook 的 updatedInput 替换后的输入。 */
|
|
58
58
|
input?: unknown;
|
|
59
|
+
/** auto 权限模式的判定(随 tool_execution_end 发出)。 */
|
|
60
|
+
autoDecision?: AutoDecision;
|
|
59
61
|
}
|
|
60
62
|
export interface LoopHooks {
|
|
61
63
|
/** 档一裁剪、Hook 的 additionalContext 注入。 */
|
|
@@ -175,6 +177,10 @@ export type SessionEvent = {
|
|
|
175
177
|
result: ToolResult;
|
|
176
178
|
isError: boolean;
|
|
177
179
|
parentToolCallId?: string;
|
|
180
|
+
/** auto 权限模式下这次调用的判定(层、结论、原因);其它模式缺省。 */
|
|
181
|
+
autoDecision?: AutoDecision;
|
|
182
|
+
/** 被拒(权限管线、PreToolUse Hook、审批或无人值守)而没有执行;原因是 `result` 的文本。 */
|
|
183
|
+
denied?: true;
|
|
178
184
|
} | {
|
|
179
185
|
type: "queue_update";
|
|
180
186
|
steering: string[];
|
|
@@ -210,6 +216,8 @@ export type SessionEvent = {
|
|
|
210
216
|
timeoutMs: number;
|
|
211
217
|
/** [W3-C0] 执行前预览(同 `ApprovalRequest.preview`)。 */
|
|
212
218
|
preview?: ActionPreview;
|
|
219
|
+
/** auto 权限模式下为什么询问(同 `ApprovalRequest.autoDecision`)。 */
|
|
220
|
+
autoDecision?: AutoDecision;
|
|
213
221
|
} | {
|
|
214
222
|
type: "permission_resolved";
|
|
215
223
|
requestId: string;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 任一出现过(含 0)即 `cacheReported: true`,之后的事件不会把它改回 false。
|
|
13
13
|
*/
|
|
14
14
|
import { AssistantEventStreamImpl } from "../event-stream.js";
|
|
15
|
-
import {
|
|
15
|
+
import { authHeaders, describeErrorJson, idleTimeoutOf, mergeHeaders, USER_AGENT, } from "../http.js";
|
|
16
16
|
import { readSseEvents } from "../sse.js";
|
|
17
17
|
import { postWithCacheFallback } from "./cache-params.js";
|
|
18
18
|
import { ANTHROPIC_VERSION, anthropicMessagesUrl, buildAnthropicRequest, detectAnthropicCompat, } from "./anthropic-request.js";
|
|
@@ -168,11 +168,12 @@ async function run(stream, model, context, options) {
|
|
|
168
168
|
body,
|
|
169
169
|
signal: options.signal,
|
|
170
170
|
timeoutMs: options.timeoutMs,
|
|
171
|
+
idleTimeoutMs: idleTimeoutOf(options),
|
|
171
172
|
onResponse: options.onResponse,
|
|
172
173
|
});
|
|
173
174
|
stream.push({ type: "start", partial: tracker.output });
|
|
174
175
|
const state = { blocks: new Map(), stop: undefined, sawMessageStop: false };
|
|
175
|
-
for await (const sse of readSseEvents(response.body, options.signal)) {
|
|
176
|
+
for await (const sse of readSseEvents(response.body, options.signal, idleTimeoutOf(options))) {
|
|
176
177
|
if (sse.event === "ping" || sse.data === "")
|
|
177
178
|
continue;
|
|
178
179
|
let data;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* - 错误体 `{error:{code,status,message}}` 重排为 `429 RESOURCE_EXHAUSTED: …`。
|
|
17
17
|
*/
|
|
18
18
|
import { AssistantEventStreamImpl } from "../event-stream.js";
|
|
19
|
-
import {
|
|
19
|
+
import { authHeaders, HttpError, idleTimeoutOf, joinUrl, mergeHeaders, postJson, USER_AGENT, } from "../http.js";
|
|
20
20
|
import { readSseEvents } from "../sse.js";
|
|
21
21
|
import { GOOGLE_BASE_URL, buildGoogleRequest, detectGoogleCompat } from "./google-request.js";
|
|
22
22
|
import { BlockTracker, ProviderStopError, STREAM_ENDED_MESSAGE, createOutput, finishDone, finishError, requireApiKey, } from "./shared.js";
|
|
@@ -196,6 +196,7 @@ async function run(stream, model, context, options) {
|
|
|
196
196
|
body: replaced === undefined ? request.body : replaced,
|
|
197
197
|
signal: options.signal,
|
|
198
198
|
timeoutMs: options.timeoutMs,
|
|
199
|
+
idleTimeoutMs: idleTimeoutOf(options),
|
|
199
200
|
onResponse: options.onResponse,
|
|
200
201
|
});
|
|
201
202
|
}
|
|
@@ -209,7 +210,7 @@ async function run(stream, model, context, options) {
|
|
|
209
210
|
idSeed: Date.now().toString(36),
|
|
210
211
|
};
|
|
211
212
|
const body = response.body;
|
|
212
|
-
for await (const sse of readSseEvents(body, options.signal)) {
|
|
213
|
+
for await (const sse of readSseEvents(body, options.signal, idleTimeoutOf(options))) {
|
|
213
214
|
const data = sse.data.trim();
|
|
214
215
|
if (data === "" || data === "[DONE]")
|
|
215
216
|
continue;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* 内容推断。
|
|
15
15
|
*/
|
|
16
16
|
import { AssistantEventStreamImpl } from "../event-stream.js";
|
|
17
|
-
import {
|
|
17
|
+
import { authHeaders, describeErrorJson, idleTimeoutOf, joinUrl, mergeHeaders, USER_AGENT, } from "../http.js";
|
|
18
18
|
import { readSseEvents } from "../sse.js";
|
|
19
19
|
import { affinityHeaders, postWithCacheFallback, resolveCacheRetention } from "./cache-params.js";
|
|
20
20
|
import { detectCompat } from "./openai-compat.js";
|
|
@@ -190,6 +190,7 @@ async function run(stream, model, context, options) {
|
|
|
190
190
|
body: replaced === undefined ? request.body : replaced,
|
|
191
191
|
signal: options.signal,
|
|
192
192
|
timeoutMs: options.timeoutMs,
|
|
193
|
+
idleTimeoutMs: idleTimeoutOf(options),
|
|
193
194
|
onResponse: options.onResponse,
|
|
194
195
|
});
|
|
195
196
|
stream.push({ type: "start", partial: tracker.output });
|
|
@@ -203,7 +204,7 @@ async function run(stream, model, context, options) {
|
|
|
203
204
|
sawDone: false,
|
|
204
205
|
};
|
|
205
206
|
const body = response.body;
|
|
206
|
-
for await (const sse of readSseEvents(body, options.signal)) {
|
|
207
|
+
for await (const sse of readSseEvents(body, options.signal, idleTimeoutOf(options))) {
|
|
207
208
|
const data = sse.data.trim();
|
|
208
209
|
if (data === "")
|
|
209
210
|
continue;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* `cacheReported: true`。
|
|
16
16
|
*/
|
|
17
17
|
import { AssistantEventStreamImpl } from "../event-stream.js";
|
|
18
|
-
import {
|
|
18
|
+
import { authHeaders, describeErrorJson, idleTimeoutOf, joinUrl, mergeHeaders, USER_AGENT, } from "../http.js";
|
|
19
19
|
import { readSseEvents } from "../sse.js";
|
|
20
20
|
import { affinityHeaders, postWithCacheFallback, resolveCacheRetention } from "./cache-params.js";
|
|
21
21
|
import { buildResponsesRequest, detectResponsesCompat, encodeMessageSignature, } from "./openai-responses-request.js";
|
|
@@ -249,6 +249,7 @@ async function run(stream, model, context, options) {
|
|
|
249
249
|
body: replaced === undefined ? request.body : replaced,
|
|
250
250
|
signal: options.signal,
|
|
251
251
|
timeoutMs: options.timeoutMs,
|
|
252
|
+
idleTimeoutMs: idleTimeoutOf(options),
|
|
252
253
|
onResponse: options.onResponse,
|
|
253
254
|
});
|
|
254
255
|
stream.push({ type: "start", partial: tracker.output });
|
|
@@ -260,7 +261,7 @@ async function run(stream, model, context, options) {
|
|
|
260
261
|
};
|
|
261
262
|
let finished = false;
|
|
262
263
|
const body = response.body;
|
|
263
|
-
for await (const sse of readSseEvents(body, options.signal)) {
|
|
264
|
+
for await (const sse of readSseEvents(body, options.signal, idleTimeoutOf(options))) {
|
|
264
265
|
const raw = sse.data.trim();
|
|
265
266
|
if (raw === "" || raw === "[DONE]")
|
|
266
267
|
continue;
|
package/dist/ai/http.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* fetch 包装(设计 §1.2 ai/http.ts):超时、头合并(值为 null 删除)、错误体读取。
|
|
3
3
|
*
|
|
4
|
-
* 代理:Node 的全局 fetch 缺省不读 HTTP(S)_PROXY。
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 代理:Node 的全局 fetch 缺省不读 HTTP(S)_PROXY。CLI 启动时由 cli/proxy.ts 调 Node 内置的
|
|
5
|
+
* `http.setGlobalProxyFromEnv()`(等价于 `NODE_USE_ENV_PROXY=1`);SDK 嵌入方自行决定。
|
|
6
|
+
* 本模块不自行实现代理。
|
|
7
|
+
*
|
|
8
|
+
* 超时:`timeoutMs` 只管到拿到响应头为止;`idleTimeoutMs`(缺省 300 s,0 关闭)既管等响应头,
|
|
9
|
+
* 也管流式读取期间两次收到数据之间的间隔(每收到一块字节即重新计时,见 sse.ts)。空闲超时抛
|
|
10
|
+
* `IdleTimeoutError`,文案含 `idle timeout`,会话层按可重试错误处理。
|
|
7
11
|
*
|
|
8
12
|
* 协议层自身**不重试**(重试在会话层,§3.6)。
|
|
9
13
|
*/
|
|
@@ -29,6 +33,21 @@ export declare class HttpError extends Error {
|
|
|
29
33
|
export declare class RequestTimeoutError extends Error {
|
|
30
34
|
constructor(timeoutMs: number);
|
|
31
35
|
}
|
|
36
|
+
/** 流空闲超时缺省值:与 `request.idleTimeoutMs` / `AMA_IDLE_TIMEOUT_MS` 的缺省一致。 */
|
|
37
|
+
export declare const DEFAULT_IDLE_TIMEOUT_MS = 300000;
|
|
38
|
+
/** 请求选项里的空闲超时;未给出用缺省值,0 或负数表示关闭(返回 undefined)。 */
|
|
39
|
+
export declare function idleTimeoutOf(options: {
|
|
40
|
+
idleTimeoutMs?: number | undefined;
|
|
41
|
+
}): number | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* 服务端在 `idleTimeoutMs` 内没有任何字节:`response` = 发出请求后迟迟没有响应头,
|
|
44
|
+
* `stream` = 流读到一半停住。文案含 `idle timeout`(重试分类据此判为可重试)。
|
|
45
|
+
*/
|
|
46
|
+
export declare class IdleTimeoutError extends Error {
|
|
47
|
+
readonly idleTimeoutMs: number;
|
|
48
|
+
readonly phase: "response" | "stream";
|
|
49
|
+
constructor(idleTimeoutMs: number, phase: "response" | "stream");
|
|
50
|
+
}
|
|
32
51
|
/**
|
|
33
52
|
* 从错误体提取文本:识别 `{error:{type,message,code}}`、`{error:"..."}`、`{message}`、
|
|
34
53
|
* `{detail}` 等常见形状;不是 JSON 则截断原文。结果形如 `429 rate_limit_error: ...`。
|
|
@@ -41,12 +60,15 @@ export interface PostOptions {
|
|
|
41
60
|
body: unknown;
|
|
42
61
|
signal: AbortSignal;
|
|
43
62
|
timeoutMs?: number | undefined;
|
|
63
|
+
/** 等响应头的空闲上限(`timeoutMs` 未给时生效);undefined 不限。 */
|
|
64
|
+
idleTimeoutMs?: number | undefined;
|
|
44
65
|
onResponse?: ((status: number, headers: Headers) => void) | undefined;
|
|
45
66
|
}
|
|
46
67
|
/**
|
|
47
|
-
* POST JSON 并返回 2xx 响应(body 是 SSE 字节流)。非 2xx → HttpError;超时 → RequestTimeoutError
|
|
48
|
-
*
|
|
49
|
-
*
|
|
68
|
+
* POST JSON 并返回 2xx 响应(body 是 SSE 字节流)。非 2xx → HttpError;超时 → RequestTimeoutError
|
|
69
|
+
* (`timeoutMs`)或 IdleTimeoutError(`idleTimeoutMs`);调用方的 signal 中止 → 原样抛 AbortError
|
|
70
|
+
* (调用方据 signal.aborted 判 aborted)。超时只覆盖到拿到响应头为止,流式读取期间的空闲由
|
|
71
|
+
* `readSseEvents` 的 `idleTimeoutMs` 控制。
|
|
50
72
|
*/
|
|
51
73
|
export declare function postJson(url: string, options: PostOptions): Promise<Response>;
|
|
52
74
|
/** undici 的 `fetch failed` 把真正原因藏在 cause 里;拼出来便于重试分类与排错。 */
|
package/dist/ai/http.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* fetch 包装(设计 §1.2 ai/http.ts):超时、头合并(值为 null 删除)、错误体读取。
|
|
3
3
|
*
|
|
4
|
-
* 代理:Node 的全局 fetch 缺省不读 HTTP(S)_PROXY。
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 代理:Node 的全局 fetch 缺省不读 HTTP(S)_PROXY。CLI 启动时由 cli/proxy.ts 调 Node 内置的
|
|
5
|
+
* `http.setGlobalProxyFromEnv()`(等价于 `NODE_USE_ENV_PROXY=1`);SDK 嵌入方自行决定。
|
|
6
|
+
* 本模块不自行实现代理。
|
|
7
|
+
*
|
|
8
|
+
* 超时:`timeoutMs` 只管到拿到响应头为止;`idleTimeoutMs`(缺省 300 s,0 关闭)既管等响应头,
|
|
9
|
+
* 也管流式读取期间两次收到数据之间的间隔(每收到一块字节即重新计时,见 sse.ts)。空闲超时抛
|
|
10
|
+
* `IdleTimeoutError`,文案含 `idle timeout`,会话层按可重试错误处理。
|
|
7
11
|
*
|
|
8
12
|
* 协议层自身**不重试**(重试在会话层,§3.6)。
|
|
9
13
|
*/
|
|
@@ -76,6 +80,33 @@ export class RequestTimeoutError extends Error {
|
|
|
76
80
|
this.name = "RequestTimeoutError";
|
|
77
81
|
}
|
|
78
82
|
}
|
|
83
|
+
/** 流空闲超时缺省值:与 `request.idleTimeoutMs` / `AMA_IDLE_TIMEOUT_MS` 的缺省一致。 */
|
|
84
|
+
export const DEFAULT_IDLE_TIMEOUT_MS = 300_000;
|
|
85
|
+
/** 请求选项里的空闲超时;未给出用缺省值,0 或负数表示关闭(返回 undefined)。 */
|
|
86
|
+
export function idleTimeoutOf(options) {
|
|
87
|
+
const value = options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
88
|
+
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
89
|
+
}
|
|
90
|
+
function formatDuration(ms) {
|
|
91
|
+
return ms >= 1000 && ms % 1000 === 0 ? `${ms / 1000} s` : `${ms} ms`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 服务端在 `idleTimeoutMs` 内没有任何字节:`response` = 发出请求后迟迟没有响应头,
|
|
95
|
+
* `stream` = 流读到一半停住。文案含 `idle timeout`(重试分类据此判为可重试)。
|
|
96
|
+
*/
|
|
97
|
+
export class IdleTimeoutError extends Error {
|
|
98
|
+
idleTimeoutMs;
|
|
99
|
+
phase;
|
|
100
|
+
constructor(idleTimeoutMs, phase) {
|
|
101
|
+
const what = phase === "response"
|
|
102
|
+
? `No response from the server within ${formatDuration(idleTimeoutMs)}`
|
|
103
|
+
: `Stream stalled: no data from the server for ${formatDuration(idleTimeoutMs)}`;
|
|
104
|
+
super(`${what} (idle timeout; adjust with request.idleTimeoutMs or AMA_IDLE_TIMEOUT_MS)`);
|
|
105
|
+
this.name = "IdleTimeoutError";
|
|
106
|
+
this.idleTimeoutMs = idleTimeoutMs;
|
|
107
|
+
this.phase = phase;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
79
110
|
const MAX_ERROR_BODY = 4000;
|
|
80
111
|
/**
|
|
81
112
|
* 从错误体提取文本:识别 `{error:{type,message,code}}`、`{error:"..."}`、`{message}`、
|
|
@@ -141,12 +172,14 @@ function parseRetryAfter(headers) {
|
|
|
141
172
|
return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
|
|
142
173
|
}
|
|
143
174
|
/**
|
|
144
|
-
* POST JSON 并返回 2xx 响应(body 是 SSE 字节流)。非 2xx → HttpError;超时 → RequestTimeoutError
|
|
145
|
-
*
|
|
146
|
-
*
|
|
175
|
+
* POST JSON 并返回 2xx 响应(body 是 SSE 字节流)。非 2xx → HttpError;超时 → RequestTimeoutError
|
|
176
|
+
* (`timeoutMs`)或 IdleTimeoutError(`idleTimeoutMs`);调用方的 signal 中止 → 原样抛 AbortError
|
|
177
|
+
* (调用方据 signal.aborted 判 aborted)。超时只覆盖到拿到响应头为止,流式读取期间的空闲由
|
|
178
|
+
* `readSseEvents` 的 `idleTimeoutMs` 控制。
|
|
147
179
|
*/
|
|
148
180
|
export async function postJson(url, options) {
|
|
149
|
-
const
|
|
181
|
+
const explicit = options.timeoutMs !== undefined && options.timeoutMs > 0;
|
|
182
|
+
const timeout = explicit ? options.timeoutMs : options.idleTimeoutMs;
|
|
150
183
|
const timer = new AbortController();
|
|
151
184
|
const handle = timeout !== undefined && timeout > 0 ? setTimeout(() => timer.abort(), timeout) : undefined;
|
|
152
185
|
const signal = AbortSignal.any([options.signal, timer.signal]);
|
|
@@ -161,7 +194,7 @@ export async function postJson(url, options) {
|
|
|
161
194
|
}
|
|
162
195
|
catch (error) {
|
|
163
196
|
if (timer.signal.aborted && !options.signal.aborted && timeout !== undefined) {
|
|
164
|
-
throw new RequestTimeoutError(timeout);
|
|
197
|
+
throw explicit ? new RequestTimeoutError(timeout) : new IdleTimeoutError(timeout, "response");
|
|
165
198
|
}
|
|
166
199
|
throw normalizeNetworkError(error);
|
|
167
200
|
}
|
|
@@ -90,10 +90,17 @@ export declare class ProviderRegistry implements ProviderRegistryApi {
|
|
|
90
90
|
/** 追加 / 替换模型(例如本地枚举得到的 ollama 模型)。 */
|
|
91
91
|
addModels(providerId: string, models: readonly Model[], source?: ModelSource): void;
|
|
92
92
|
private synthesize;
|
|
93
|
-
/** 按渠道取模型:渠道不在模型的 channels 里 →
|
|
93
|
+
/** 按渠道取模型:渠道不在模型的 channels 里 → channel_not_found,列出可用渠道。 */
|
|
94
94
|
private withChannel;
|
|
95
|
+
/**
|
|
96
|
+
* `provider/model`、`provider/model@渠道` 或裸模型 id。`@` 先按渠道解析(id 里本来就带 `@` 且已登记的
|
|
97
|
+
* 模型除外),中转供应商的「未登记即合成」不会把 `@渠道` 吞进模型 id。失败的 reason:供应商不存在 →
|
|
98
|
+
* provider_not_found(候选是编辑距离最近的供应商);渠道不存在 → channel_not_found;模型不存在 →
|
|
99
|
+
* not_found(候选是最接近的模型)。
|
|
100
|
+
*/
|
|
95
101
|
findModel(ref: string): ModelLookup;
|
|
96
102
|
private findPlain;
|
|
103
|
+
/** 最接近的模型(包含关系优先,其次编辑距离),`provider/id` 形式。 */
|
|
97
104
|
private similar;
|
|
98
105
|
hasConfiguredKey(providerId: string): boolean;
|
|
99
106
|
resolveApiKey(providerId: string, channel?: string): Promise<ApiKeyResolution>;
|
|
@@ -30,6 +30,7 @@ import { BUILTIN_PROVIDERS, fallbackEnvKey, isRelayedBaseUrl } from "./builtin.j
|
|
|
30
30
|
import { applyModelOverride, loadBuiltinCatalog, toModel, withCustomDefaults } from "./catalog.js";
|
|
31
31
|
import { channelKeyId, modelChannels, parseChannels, splitChannelRef } from "./channels.js";
|
|
32
32
|
import { catalogMetadata, enrichEntry, lazyIndex, } from "./enrich.js";
|
|
33
|
+
import { closest } from "./suggest.js";
|
|
33
34
|
/**
|
|
34
35
|
* 物化:模型补齐供应商级字段(不可变:返回新对象)。给了 `channel` 时协议与地址取渠道的(模型级
|
|
35
36
|
* `explicit` 覆盖优先),headers / compat 按 供应商 ← 渠道 ← 模型 合并,并记下渠道名。
|
|
@@ -326,36 +327,40 @@ export class ProviderRegistry {
|
|
|
326
327
|
const channelData = provider.channels?.find((c) => c.name === (channel ?? model.channels?.[0]));
|
|
327
328
|
return materializeModel(model, provider, channelData);
|
|
328
329
|
}
|
|
329
|
-
/** 按渠道取模型:渠道不在模型的 channels 里 →
|
|
330
|
+
/** 按渠道取模型:渠道不在模型的 channels 里 → channel_not_found,列出可用渠道。 */
|
|
330
331
|
withChannel(provider, model, channel) {
|
|
331
332
|
const available = model.channels ?? [];
|
|
332
|
-
if (!available.includes(channel)) {
|
|
333
|
+
if (!available.includes(channel) || !provider.channels?.some((c) => c.name === channel)) {
|
|
333
334
|
return {
|
|
334
335
|
ok: false,
|
|
335
|
-
reason: "
|
|
336
|
-
candidates: available.map((c) => `${provider.id}/${model.id}@${c}`),
|
|
336
|
+
reason: "channel_not_found",
|
|
337
|
+
candidates: available.filter(Boolean).map((c) => `${provider.id}/${model.id}@${c}`),
|
|
337
338
|
};
|
|
338
339
|
}
|
|
339
340
|
if (channel === model.channel)
|
|
340
341
|
return { ok: true, model, provider };
|
|
341
342
|
return { ok: true, model: this.materialize(provider, model, channel), provider };
|
|
342
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* `provider/model`、`provider/model@渠道` 或裸模型 id。`@` 先按渠道解析(id 里本来就带 `@` 且已登记的
|
|
346
|
+
* 模型除外),中转供应商的「未登记即合成」不会把 `@渠道` 吞进模型 id。失败的 reason:供应商不存在 →
|
|
347
|
+
* provider_not_found(候选是编辑距离最近的供应商);渠道不存在 → channel_not_found;模型不存在 →
|
|
348
|
+
* not_found(候选是最接近的模型)。
|
|
349
|
+
*/
|
|
343
350
|
findModel(ref) {
|
|
344
351
|
const trimmed = ref.trim();
|
|
345
|
-
const direct = this.findPlain(trimmed);
|
|
346
|
-
if (direct.ok)
|
|
347
|
-
return direct;
|
|
348
352
|
const split = splitChannelRef(trimmed);
|
|
349
353
|
if (split === undefined)
|
|
350
|
-
return
|
|
354
|
+
return this.findPlain(trimmed);
|
|
355
|
+
const exact = this.findPlain(trimmed, undefined, false);
|
|
356
|
+
if (exact.ok)
|
|
357
|
+
return exact;
|
|
351
358
|
const base = this.findPlain(split.base, split.channel);
|
|
352
359
|
if (!base.ok)
|
|
353
|
-
return base
|
|
354
|
-
if (!base.provider.channels?.some((c) => c.name === split.channel))
|
|
355
|
-
return direct;
|
|
360
|
+
return base;
|
|
356
361
|
return this.withChannel(base.provider, base.model, split.channel);
|
|
357
362
|
}
|
|
358
|
-
findPlain(trimmed, channel) {
|
|
363
|
+
findPlain(trimmed, channel, synthesize = true) {
|
|
359
364
|
const slash = trimmed.indexOf("/");
|
|
360
365
|
if (slash > 0) {
|
|
361
366
|
const provider = this.providers.get(trimmed.slice(0, slash));
|
|
@@ -364,7 +369,8 @@ export class ProviderRegistry {
|
|
|
364
369
|
const model = provider.models.find((m) => m.id === id);
|
|
365
370
|
if (model)
|
|
366
371
|
return { ok: true, model, provider };
|
|
367
|
-
|
|
372
|
+
const relayed = provider.models.length === 0 || this.relayed.has(provider.id);
|
|
373
|
+
if (synthesize && relayed && id.length > 0) {
|
|
368
374
|
const synthesized = this.synthesize(provider, id);
|
|
369
375
|
if (channel !== undefined && provider.channels?.some((c) => c.name === channel))
|
|
370
376
|
synthesized.channels = [...new Set([...(synthesized.channels ?? []), channel])];
|
|
@@ -374,6 +380,14 @@ export class ProviderRegistry {
|
|
|
374
380
|
}
|
|
375
381
|
}
|
|
376
382
|
const matches = this.listModels().filter((entry) => entry.model.id === trimmed);
|
|
383
|
+
if (matches.length === 0 && slash > 0) {
|
|
384
|
+
const ids = [...this.providers.keys()];
|
|
385
|
+
return {
|
|
386
|
+
ok: false,
|
|
387
|
+
reason: "provider_not_found",
|
|
388
|
+
candidates: closest(trimmed.slice(0, slash), ids),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
377
391
|
if (matches.length === 1 && matches[0]) {
|
|
378
392
|
return { ok: true, model: matches[0].model, provider: matches[0].provider };
|
|
379
393
|
}
|
|
@@ -390,16 +404,10 @@ export class ProviderRegistry {
|
|
|
390
404
|
candidates: (configured.length > 0 ? configured : matches).map((entry) => `${entry.provider.id}/${entry.model.id}`),
|
|
391
405
|
};
|
|
392
406
|
}
|
|
407
|
+
/** 最接近的模型(包含关系优先,其次编辑距离),`provider/id` 形式。 */
|
|
393
408
|
similar(id, providerId) {
|
|
394
|
-
const
|
|
395
|
-
return
|
|
396
|
-
.filter((entry) => providerId === undefined || entry.provider.id === providerId)
|
|
397
|
-
.filter((entry) => {
|
|
398
|
-
const candidate = entry.model.id.toLowerCase();
|
|
399
|
-
return needle.length > 0 && (candidate.includes(needle) || needle.includes(candidate));
|
|
400
|
-
})
|
|
401
|
-
.slice(0, 10)
|
|
402
|
-
.map((entry) => `${entry.provider.id}/${entry.model.id}`);
|
|
409
|
+
const entries = this.listModels().filter((entry) => providerId === undefined || entry.provider.id === providerId);
|
|
410
|
+
return closest(id, entries, 5, (entry) => entry.model.id).map((entry) => `${entry.provider.id}/${entry.model.id}`);
|
|
403
411
|
}
|
|
404
412
|
hasConfiguredKey(providerId) {
|
|
405
413
|
const provider = this.providers.get(providerId);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型 / 供应商 / 渠道写错时的候选与报错文案(W4-C)。
|
|
3
|
+
*
|
|
4
|
+
* 候选排序:包含关系(`sonnet` ⊂ `claude-sonnet-5`)优先,其次按编辑距离;距离超过
|
|
5
|
+
* `max(2, ⌈长度 / 3⌉)` 的不算接近。只比较 id 本身(不含 `provider/` 前缀),大小写不敏感。
|
|
6
|
+
*/
|
|
7
|
+
import type { ModelLookup } from "../types.js";
|
|
8
|
+
/** Levenshtein 距离(插入 / 删除 / 替换各计 1)。 */
|
|
9
|
+
export declare function editDistance(a: string, b: string): number;
|
|
10
|
+
/**
|
|
11
|
+
* 从 `candidates` 里挑与 `needle` 最接近的至多 `limit` 个。`key` 取参与比较的部分
|
|
12
|
+
* (缺省整串),返回原值。
|
|
13
|
+
*/
|
|
14
|
+
export declare function closest<T>(needle: string, candidates: readonly T[], limit?: number, key?: (item: T) => string): T[];
|
|
15
|
+
/** 解析失败的一行说明(不含前缀标签):`渠道不存在:x;该模型可用渠道:…` 等。 */
|
|
16
|
+
export declare function describeLookupFailure(ref: string, failure: Extract<ModelLookup, {
|
|
17
|
+
ok: false;
|
|
18
|
+
}>): string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型 / 供应商 / 渠道写错时的候选与报错文案(W4-C)。
|
|
3
|
+
*
|
|
4
|
+
* 候选排序:包含关系(`sonnet` ⊂ `claude-sonnet-5`)优先,其次按编辑距离;距离超过
|
|
5
|
+
* `max(2, ⌈长度 / 3⌉)` 的不算接近。只比较 id 本身(不含 `provider/` 前缀),大小写不敏感。
|
|
6
|
+
*/
|
|
7
|
+
/** Levenshtein 距离(插入 / 删除 / 替换各计 1)。 */
|
|
8
|
+
export function editDistance(a, b) {
|
|
9
|
+
if (a === b)
|
|
10
|
+
return 0;
|
|
11
|
+
if (a.length === 0)
|
|
12
|
+
return b.length;
|
|
13
|
+
if (b.length === 0)
|
|
14
|
+
return a.length;
|
|
15
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
16
|
+
for (let i = 1; i <= a.length; i++) {
|
|
17
|
+
const row = [i];
|
|
18
|
+
for (let j = 1; j <= b.length; j++) {
|
|
19
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
20
|
+
row[j] = Math.min((prev[j] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
21
|
+
}
|
|
22
|
+
prev = row;
|
|
23
|
+
}
|
|
24
|
+
return prev[b.length] ?? 0;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 从 `candidates` 里挑与 `needle` 最接近的至多 `limit` 个。`key` 取参与比较的部分
|
|
28
|
+
* (缺省整串),返回原值。
|
|
29
|
+
*/
|
|
30
|
+
export function closest(needle, candidates, limit = 5, key = String) {
|
|
31
|
+
const target = needle.toLowerCase();
|
|
32
|
+
if (target === "")
|
|
33
|
+
return [];
|
|
34
|
+
const budget = Math.max(2, Math.ceil(target.length / 3));
|
|
35
|
+
const scored = [];
|
|
36
|
+
candidates.forEach((item, index) => {
|
|
37
|
+
const value = key(item).toLowerCase();
|
|
38
|
+
if (value === "")
|
|
39
|
+
return;
|
|
40
|
+
if (value.includes(target) || target.includes(value)) {
|
|
41
|
+
scored.push({ item, score: Math.abs(value.length - target.length) / 1000, index });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const distance = editDistance(target, value);
|
|
45
|
+
if (distance <= budget)
|
|
46
|
+
scored.push({ item, score: distance, index });
|
|
47
|
+
});
|
|
48
|
+
return scored
|
|
49
|
+
.sort((a, b) => a.score - b.score || a.index - b.index)
|
|
50
|
+
.slice(0, limit)
|
|
51
|
+
.map((entry) => entry.item);
|
|
52
|
+
}
|
|
53
|
+
/** 解析失败的一行说明(不含前缀标签):`渠道不存在:x;该模型可用渠道:…` 等。 */
|
|
54
|
+
export function describeLookupFailure(ref, failure) {
|
|
55
|
+
const list = failure.candidates.slice(0, 20).join(", ");
|
|
56
|
+
switch (failure.reason) {
|
|
57
|
+
case "channel_not_found":
|
|
58
|
+
return failure.candidates.length > 0
|
|
59
|
+
? `渠道不存在:${ref};该模型可用渠道:${list}`
|
|
60
|
+
: `渠道不存在:${ref};该模型没有可选渠道(去掉 @ 后缀)`;
|
|
61
|
+
case "provider_not_found":
|
|
62
|
+
return failure.candidates.length > 0
|
|
63
|
+
? `供应商不存在:${ref};最接近的供应商:${list}`
|
|
64
|
+
: `供应商不存在:${ref}(ama providers list 查看已配置的供应商)`;
|
|
65
|
+
case "ambiguous":
|
|
66
|
+
return `模型名有歧义:${ref};候选:${list}`;
|
|
67
|
+
default:
|
|
68
|
+
return failure.candidates.length > 0
|
|
69
|
+
? `模型不存在:${ref};最接近的模型:${list}`
|
|
70
|
+
: `模型不存在:${ref}(ama models list 查看可用模型)`;
|
|
71
|
+
}
|
|
72
|
+
}
|
package/dist/ai/sse.d.ts
CHANGED
|
@@ -28,5 +28,9 @@ export declare class SseParser {
|
|
|
28
28
|
private processLine;
|
|
29
29
|
private dispatch;
|
|
30
30
|
}
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* 从字节流读 SSE 事件;`signal` 中止时停止读取(调用方据此产出 aborted)。
|
|
33
|
+
* `idleTimeoutMs`:等待下一块字节的上限,每收到一块即重新计时(消费者处理事件的时间不计);
|
|
34
|
+
* 超时取消底层流并抛 `IdleTimeoutError`(phase `stream`)。
|
|
35
|
+
*/
|
|
36
|
+
export declare function readSseEvents(body: ReadableStream<Uint8Array>, signal?: AbortSignal, idleTimeoutMs?: number): AsyncGenerator<SseEvent>;
|