c0de-agent 0.0.0 → 1.0.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/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/cli/commands/acp.d.ts +19 -0
- package/dist/cli/commands/acp.js +50 -0
- package/dist/cli/commands/chat.d.ts +13 -0
- package/dist/cli/commands/chat.js +26 -0
- package/dist/cli/commands/config.d.ts +17 -0
- package/dist/cli/commands/config.js +77 -0
- package/dist/cli/commands/init.d.ts +9 -0
- package/dist/cli/commands/init.js +14 -0
- package/dist/cli/commands/plugin.d.ts +11 -0
- package/dist/cli/commands/plugin.js +31 -0
- package/dist/cli/commands/serve.d.ts +18 -0
- package/dist/cli/commands/serve.js +35 -0
- package/dist/cli/commands/update.d.ts +24 -0
- package/dist/cli/commands/update.js +40 -0
- package/dist/cli/deps.d.ts +19 -0
- package/dist/cli/deps.js +57 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.js +149 -0
- package/dist/cli/modes/acp.d.ts +18 -0
- package/dist/cli/modes/acp.js +57 -0
- package/dist/cli/modes/print.d.ts +16 -0
- package/dist/cli/modes/print.js +30 -0
- package/dist/cli/parser.d.ts +19 -0
- package/dist/cli/parser.js +28 -0
- package/dist/cli/utils/output.d.ts +11 -0
- package/dist/cli/utils/output.js +23 -0
- package/dist/cli/utils/prompt.d.ts +12 -0
- package/dist/cli/utils/prompt.js +18 -0
- package/dist/core/agent.d.ts +12 -0
- package/dist/core/agent.js +95 -0
- package/dist/core/agents/builtin.d.ts +6 -0
- package/dist/core/agents/builtin.js +76 -0
- package/dist/core/agents/discovery.d.ts +6 -0
- package/dist/core/agents/discovery.js +96 -0
- package/dist/core/agents/index.d.ts +6 -0
- package/dist/core/agents/index.js +4 -0
- package/dist/core/agents/parallel.d.ts +13 -0
- package/dist/core/agents/parallel.js +37 -0
- package/dist/core/agents/registry.d.ts +4 -0
- package/dist/core/agents/registry.js +23 -0
- package/dist/core/agents/types.d.ts +37 -0
- package/dist/core/agents/types.js +1 -0
- package/dist/core/compact.d.ts +11 -0
- package/dist/core/compact.js +31 -0
- package/dist/core/config.d.ts +7 -0
- package/dist/core/config.js +84 -0
- package/dist/core/context.d.ts +23 -0
- package/dist/core/context.js +93 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +11 -0
- package/dist/core/loop.d.ts +25 -0
- package/dist/core/loop.js +651 -0
- package/dist/core/metrics.d.ts +27 -0
- package/dist/core/metrics.js +94 -0
- package/dist/core/prompt-registry.d.ts +21 -0
- package/dist/core/prompt-registry.js +200 -0
- package/dist/core/prompt.d.ts +11 -0
- package/dist/core/prompt.js +13 -0
- package/dist/core/secret.d.ts +12 -0
- package/dist/core/secret.js +51 -0
- package/dist/core/slash.d.ts +15 -0
- package/dist/core/slash.js +114 -0
- package/dist/core/steering.d.ts +8 -0
- package/dist/core/steering.js +15 -0
- package/dist/core/title.d.ts +42 -0
- package/dist/core/title.js +115 -0
- package/dist/core/tool-exec.d.ts +20 -0
- package/dist/core/tool-exec.js +64 -0
- package/dist/core/types.d.ts +95 -0
- package/dist/core/types.js +1 -0
- package/dist/core/worktree.d.ts +24 -0
- package/dist/core/worktree.js +68 -0
- package/dist/dap/index.d.ts +6 -0
- package/dist/dap/index.js +4 -0
- package/dist/dap/protocol.d.ts +40 -0
- package/dist/dap/protocol.js +132 -0
- package/dist/dap/session.d.ts +23 -0
- package/dist/dap/session.js +130 -0
- package/dist/dap/transport.d.ts +8 -0
- package/dist/dap/transport.js +30 -0
- package/dist/dap/types.d.ts +50 -0
- package/dist/dap/types.js +2 -0
- package/dist/db/client.d.ts +22 -0
- package/dist/db/client.js +39 -0
- package/dist/db/index.d.ts +6 -0
- package/dist/db/index.js +4 -0
- package/dist/db/migrate.d.ts +9 -0
- package/dist/db/migrate.js +15 -0
- package/dist/db/schema.d.ts +965 -0
- package/dist/db/schema.js +112 -0
- package/dist/db/types.d.ts +11 -0
- package/dist/db/types.js +1 -0
- package/dist/llm/index.d.ts +13 -0
- package/dist/llm/index.js +8 -0
- package/dist/llm/protocols/openai-compat.d.ts +139 -0
- package/dist/llm/protocols/openai-compat.js +214 -0
- package/dist/llm/protocols/utils/index.d.ts +2 -0
- package/dist/llm/protocols/utils/index.js +2 -0
- package/dist/llm/protocols/utils/lifecycle.d.ts +23 -0
- package/dist/llm/protocols/utils/lifecycle.js +46 -0
- package/dist/llm/protocols/utils/tool-stream.d.ts +46 -0
- package/dist/llm/protocols/utils/tool-stream.js +94 -0
- package/dist/llm/provider-error.d.ts +5 -0
- package/dist/llm/provider-error.js +33 -0
- package/dist/llm/provider.d.ts +32 -0
- package/dist/llm/provider.js +191 -0
- package/dist/llm/registry.d.ts +44 -0
- package/dist/llm/registry.js +107 -0
- package/dist/llm/retry.d.ts +37 -0
- package/dist/llm/retry.js +82 -0
- package/dist/llm/routing.d.ts +36 -0
- package/dist/llm/routing.js +53 -0
- package/dist/llm/schema/errors.d.ts +88 -0
- package/dist/llm/schema/errors.js +33 -0
- package/dist/llm/schema/events.d.ts +135 -0
- package/dist/llm/schema/events.js +35 -0
- package/dist/llm/schema/ids.d.ts +22 -0
- package/dist/llm/schema/ids.js +3 -0
- package/dist/llm/schema/index.d.ts +5 -0
- package/dist/llm/schema/index.js +5 -0
- package/dist/llm/schema/messages.d.ts +97 -0
- package/dist/llm/schema/messages.js +18 -0
- package/dist/llm/schema/options.d.ts +54 -0
- package/dist/llm/schema/options.js +54 -0
- package/dist/llm/token.d.ts +2 -0
- package/dist/llm/token.js +13 -0
- package/dist/llm/transport.d.ts +31 -0
- package/dist/llm/transport.js +165 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +3 -0
- package/dist/plugins/builtin.d.ts +6 -0
- package/dist/plugins/builtin.js +54 -0
- package/dist/plugins/hooks.d.ts +3 -0
- package/dist/plugins/hooks.js +79 -0
- package/dist/plugins/index.d.ts +9 -0
- package/dist/plugins/index.js +8 -0
- package/dist/plugins/init.d.ts +23 -0
- package/dist/plugins/init.js +30 -0
- package/dist/plugins/lifecycle.d.ts +6 -0
- package/dist/plugins/lifecycle.js +74 -0
- package/dist/plugins/loader.d.ts +16 -0
- package/dist/plugins/loader.js +68 -0
- package/dist/plugins/logger.d.ts +4 -0
- package/dist/plugins/logger.js +31 -0
- package/dist/plugins/registry.d.ts +7 -0
- package/dist/plugins/registry.js +24 -0
- package/dist/plugins/types.d.ts +113 -0
- package/dist/plugins/types.js +1 -0
- package/dist/project/detect.d.ts +4 -0
- package/dist/project/detect.js +131 -0
- package/dist/project/index.d.ts +4 -0
- package/dist/project/index.js +2 -0
- package/dist/project/project.d.ts +23 -0
- package/dist/project/project.js +87 -0
- package/dist/project/resolve.d.ts +8 -0
- package/dist/project/resolve.js +81 -0
- package/dist/server/agent-manager.d.ts +32 -0
- package/dist/server/agent-manager.js +54 -0
- package/dist/server/app.d.ts +5 -0
- package/dist/server/app.js +75 -0
- package/dist/server/context.d.ts +20 -0
- package/dist/server/context.js +47 -0
- package/dist/server/dev.d.ts +11 -0
- package/dist/server/dev.js +83 -0
- package/dist/server/index.d.ts +11 -0
- package/dist/server/index.js +7 -0
- package/dist/server/middleware/auth.d.ts +15 -0
- package/dist/server/middleware/auth.js +29 -0
- package/dist/server/middleware/cors.d.ts +16 -0
- package/dist/server/middleware/cors.js +44 -0
- package/dist/server/middleware/error.d.ts +15 -0
- package/dist/server/middleware/error.js +11 -0
- package/dist/server/permission/interactive.d.ts +24 -0
- package/dist/server/permission/interactive.js +56 -0
- package/dist/server/permission/store.d.ts +30 -0
- package/dist/server/permission/store.js +47 -0
- package/dist/server/routes/agent.d.ts +4 -0
- package/dist/server/routes/agent.js +19 -0
- package/dist/server/routes/catalog.d.ts +42 -0
- package/dist/server/routes/catalog.js +194 -0
- package/dist/server/routes/chat.d.ts +8 -0
- package/dist/server/routes/chat.js +282 -0
- package/dist/server/routes/commands.d.ts +5 -0
- package/dist/server/routes/commands.js +17 -0
- package/dist/server/routes/config.d.ts +4 -0
- package/dist/server/routes/config.js +27 -0
- package/dist/server/routes/files.d.ts +4 -0
- package/dist/server/routes/files.js +183 -0
- package/dist/server/routes/filesystem.d.ts +29 -0
- package/dist/server/routes/filesystem.js +108 -0
- package/dist/server/routes/health.d.ts +3 -0
- package/dist/server/routes/health.js +13 -0
- package/dist/server/routes/permissions.d.ts +5 -0
- package/dist/server/routes/permissions.js +23 -0
- package/dist/server/routes/project.d.ts +4 -0
- package/dist/server/routes/project.js +51 -0
- package/dist/server/routes/provider.d.ts +16 -0
- package/dist/server/routes/provider.js +66 -0
- package/dist/server/routes/session.d.ts +4 -0
- package/dist/server/routes/session.js +130 -0
- package/dist/server/routes/tool.d.ts +4 -0
- package/dist/server/routes/tool.js +28 -0
- package/dist/server/routes/update.d.ts +14 -0
- package/dist/server/routes/update.js +49 -0
- package/dist/server/server.d.ts +43 -0
- package/dist/server/server.js +197 -0
- package/dist/server/types.d.ts +73 -0
- package/dist/server/types.js +2 -0
- package/dist/server/util/safe-path.d.ts +7 -0
- package/dist/server/util/safe-path.js +7 -0
- package/dist/session/archive.d.ts +15 -0
- package/dist/session/archive.js +129 -0
- package/dist/session/branch.d.ts +9 -0
- package/dist/session/branch.js +63 -0
- package/dist/session/compaction.d.ts +27 -0
- package/dist/session/compaction.js +192 -0
- package/dist/session/context.d.ts +16 -0
- package/dist/session/context.js +173 -0
- package/dist/session/index.d.ts +10 -0
- package/dist/session/index.js +10 -0
- package/dist/session/message.d.ts +22 -0
- package/dist/session/message.js +145 -0
- package/dist/session/session.d.ts +37 -0
- package/dist/session/session.js +144 -0
- package/dist/session/snapshot.d.ts +11 -0
- package/dist/session/snapshot.js +60 -0
- package/dist/session/squash.d.ts +9 -0
- package/dist/session/squash.js +88 -0
- package/dist/session/token.d.ts +10 -0
- package/dist/session/token.js +33 -0
- package/dist/session/types.d.ts +122 -0
- package/dist/session/types.js +1 -0
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/types/agent.d.ts +202 -0
- package/dist/shared/types/agent.js +1 -0
- package/dist/shared/types/base.d.ts +32 -0
- package/dist/shared/types/base.js +1 -0
- package/dist/shared/types/config.d.ts +104 -0
- package/dist/shared/types/config.js +1 -0
- package/dist/shared/types/index.d.ts +6 -0
- package/dist/shared/types/index.js +1 -0
- package/dist/shared/types/llm.d.ts +122 -0
- package/dist/shared/types/llm.js +1 -0
- package/dist/shared/types/message.d.ts +71 -0
- package/dist/shared/types/message.js +1 -0
- package/dist/shared/types/tool.d.ts +134 -0
- package/dist/shared/types/tool.js +1 -0
- package/dist/shared/utils/path.d.ts +15 -0
- package/dist/shared/utils/path.js +23 -0
- package/dist/shared/utils.d.ts +5 -0
- package/dist/shared/utils.js +10 -0
- package/dist/tools/builtin/bash.d.ts +12 -0
- package/dist/tools/builtin/bash.js +121 -0
- package/dist/tools/builtin/dap.d.ts +23 -0
- package/dist/tools/builtin/dap.js +231 -0
- package/dist/tools/builtin/edit.d.ts +12 -0
- package/dist/tools/builtin/edit.js +151 -0
- package/dist/tools/builtin/glob.d.ts +11 -0
- package/dist/tools/builtin/glob.js +120 -0
- package/dist/tools/builtin/grep.d.ts +6 -0
- package/dist/tools/builtin/grep.js +141 -0
- package/dist/tools/builtin/read.d.ts +6 -0
- package/dist/tools/builtin/read.js +73 -0
- package/dist/tools/builtin/resolvers.d.ts +12 -0
- package/dist/tools/builtin/resolvers.js +79 -0
- package/dist/tools/builtin/task.d.ts +9 -0
- package/dist/tools/builtin/task.js +100 -0
- package/dist/tools/builtin/write.d.ts +6 -0
- package/dist/tools/builtin/write.js +36 -0
- package/dist/tools/builtin/yield.d.ts +9 -0
- package/dist/tools/builtin/yield.js +40 -0
- package/dist/tools/executor.d.ts +9 -0
- package/dist/tools/executor.js +54 -0
- package/dist/tools/hashline/index.d.ts +2 -0
- package/dist/tools/hashline/index.js +3 -0
- package/dist/tools/hashline/patch.d.ts +48 -0
- package/dist/tools/hashline/patch.js +201 -0
- package/dist/tools/index.d.ts +30 -0
- package/dist/tools/index.js +55 -0
- package/dist/tools/permission.d.ts +13 -0
- package/dist/tools/permission.js +39 -0
- package/dist/tools/registry.d.ts +15 -0
- package/dist/tools/registry.js +54 -0
- package/dist/tools/resolver.d.ts +15 -0
- package/dist/tools/resolver.js +47 -0
- package/dist/tools/truncate.d.ts +8 -0
- package/dist/tools/truncate.js +50 -0
- package/dist/tools/types.d.ts +101 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/validate.d.ts +8 -0
- package/dist/tools/validate.js +130 -0
- package/dist/tools/websearch/fetch.d.ts +11 -0
- package/dist/tools/websearch/fetch.js +21 -0
- package/dist/tools/websearch/index.d.ts +19 -0
- package/dist/tools/websearch/index.js +78 -0
- package/dist/tools/websearch/providers/brave.d.ts +5 -0
- package/dist/tools/websearch/providers/brave.js +53 -0
- package/dist/tools/websearch/providers/duckduckgo.d.ts +3 -0
- package/dist/tools/websearch/providers/duckduckgo.js +75 -0
- package/dist/tools/websearch/providers/tavily.d.ts +11 -0
- package/dist/tools/websearch/providers/tavily.js +89 -0
- package/dist/tools/websearch/types.d.ts +51 -0
- package/dist/tools/websearch/types.js +18 -0
- package/dist/tools/websearch/websearch.d.ts +10 -0
- package/dist/tools/websearch/websearch.js +48 -0
- package/dist/update/index.d.ts +10 -0
- package/dist/update/index.js +6 -0
- package/dist/update/ipc.d.ts +15 -0
- package/dist/update/ipc.js +41 -0
- package/dist/update/scheduler.d.ts +34 -0
- package/dist/update/scheduler.js +57 -0
- package/dist/update/snapshot.d.ts +39 -0
- package/dist/update/snapshot.js +99 -0
- package/dist/update/updater.d.ts +48 -0
- package/dist/update/updater.js +83 -0
- package/dist/update/version.d.ts +18 -0
- package/dist/update/version.js +49 -0
- package/drizzle/0000_magical_nightshade.sql +55 -0
- package/drizzle/0001_chemical_doctor_faustus.sql +13 -0
- package/drizzle/0002_amusing_micromacro.sql +12 -0
- package/drizzle/0003_regular_purifiers.sql +2 -0
- package/drizzle/meta/0000_snapshot.json +448 -0
- package/drizzle/meta/0001_snapshot.json +539 -0
- package/drizzle/meta/0002_snapshot.json +633 -0
- package/drizzle/meta/0003_snapshot.json +645 -0
- package/drizzle/meta/_journal.json +34 -0
- package/package.json +100 -8
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { isLLMError, llmError, reasonMessage } from '../../schema/errors.js';
|
|
2
|
+
const empty = () => ({});
|
|
3
|
+
/**
|
|
4
|
+
* Append a tool-call delta. If the index is new, emit a `tool-input-start`
|
|
5
|
+
* (using the delta's id/name if present, else synthesized). Then emit a
|
|
6
|
+
* `tool-input-delta` when an argument fragment is present.
|
|
7
|
+
*/
|
|
8
|
+
const appendOrStart = (state, delta) => {
|
|
9
|
+
const current = state[delta.index];
|
|
10
|
+
const events = [];
|
|
11
|
+
let next;
|
|
12
|
+
if (current === undefined) {
|
|
13
|
+
const id = delta.id;
|
|
14
|
+
const name = delta.name;
|
|
15
|
+
// 无效 id/name(缺失或空字符串):丢弃该 delta 而非抛错。部分兼容
|
|
16
|
+
// provider 把 tool call 的 arguments 流式片段拆成多个独立 delta,每片
|
|
17
|
+
// id/name 为空。接受空值会产生空 id 的 tool call,导致下一轮
|
|
18
|
+
// invalid tool_call_id;抛错则让整个流崩溃。跳过最稳妥。
|
|
19
|
+
if (!id || !name) {
|
|
20
|
+
return { state, events };
|
|
21
|
+
}
|
|
22
|
+
next = { id, name, input: '', started: false };
|
|
23
|
+
const start = { type: 'tool-input-start', id, name };
|
|
24
|
+
events.push(start);
|
|
25
|
+
next.started = true;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
next = { ...current };
|
|
29
|
+
}
|
|
30
|
+
if (delta.argumentsDelta !== undefined && delta.argumentsDelta.length > 0) {
|
|
31
|
+
next.input += delta.argumentsDelta;
|
|
32
|
+
const d = {
|
|
33
|
+
type: 'tool-input-delta',
|
|
34
|
+
id: next.id,
|
|
35
|
+
name: next.name,
|
|
36
|
+
text: delta.argumentsDelta,
|
|
37
|
+
};
|
|
38
|
+
events.push(d);
|
|
39
|
+
}
|
|
40
|
+
return { state: { ...state, [delta.index]: next }, events };
|
|
41
|
+
};
|
|
42
|
+
/** Parse a raw JSON arguments string; empty string becomes `{}`. */
|
|
43
|
+
const parseToolInput = (raw) => {
|
|
44
|
+
const source = raw.length === 0 ? '{}' : raw;
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(source);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw llmError('ProviderShared', 'stream', {
|
|
50
|
+
_tag: 'InvalidProviderOutput',
|
|
51
|
+
message: `Invalid JSON tool arguments: ${source}`,
|
|
52
|
+
raw: source,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Finalize all pending tool calls (OpenAI Chat style — no per-tool stop event).
|
|
58
|
+
* Emits `tool-input-end` + parsed `tool-call` for each, and clears state.
|
|
59
|
+
*/
|
|
60
|
+
const finishAll = (state) => {
|
|
61
|
+
const events = [];
|
|
62
|
+
const tools = [];
|
|
63
|
+
for (const key of Object.keys(state)) {
|
|
64
|
+
const tool = state[Number(key)];
|
|
65
|
+
if (tool === undefined)
|
|
66
|
+
continue;
|
|
67
|
+
const end = { type: 'tool-input-end', id: tool.id, name: tool.name };
|
|
68
|
+
events.push(end);
|
|
69
|
+
// 解析失败不抛错:模型可能输出不完整 JSON(流被截断/提前结束)。
|
|
70
|
+
// 标记 _parseError + _raw 让流完整结束,由 agent loop 把错误反馈给模型重试,
|
|
71
|
+
// 而非让整个会话崩溃。对齐 oh-my-pi 的 __parseError 容错(agent-loop.ts:1741)。
|
|
72
|
+
let input;
|
|
73
|
+
try {
|
|
74
|
+
input = parseToolInput(tool.input);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
// parseToolInput 抛的是 llmError(普通对象,非 Error 实例),直接 String(e)
|
|
78
|
+
// 会退化成 "[object Object]"。按错误类型提取可读消息。
|
|
79
|
+
input = {
|
|
80
|
+
_parseError: isLLMError(e)
|
|
81
|
+
? reasonMessage(e.reason)
|
|
82
|
+
: e instanceof Error
|
|
83
|
+
? e.message
|
|
84
|
+
: String(e),
|
|
85
|
+
_raw: tool.input,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
tools.push({ id: tool.id, name: tool.name, input });
|
|
89
|
+
const call = { type: 'tool-call', id: tool.id, name: tool.name, input };
|
|
90
|
+
events.push(call);
|
|
91
|
+
}
|
|
92
|
+
return { state: {}, events, tools };
|
|
93
|
+
};
|
|
94
|
+
export { appendOrStart, empty, finishAll, parseToolInput };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** True when a provider error message indicates the input exceeded the context window. */
|
|
2
|
+
declare const isContextOverflow: (message: string) => boolean;
|
|
3
|
+
/** True when a thrown/produced value represents a context-overflow failure. */
|
|
4
|
+
declare const isContextOverflowFailure: (failure: unknown) => boolean;
|
|
5
|
+
export { isContextOverflow, isContextOverflowFailure };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { isLLMError } from './schema/errors.js';
|
|
2
|
+
const patterns = [
|
|
3
|
+
/prompt is too long/i,
|
|
4
|
+
/input is too long for requested model/i,
|
|
5
|
+
/exceeds the context window/i,
|
|
6
|
+
/input token count.*exceeds the maximum/i,
|
|
7
|
+
/maximum prompt length is \d+/i,
|
|
8
|
+
/reduce the length of the messages/i,
|
|
9
|
+
/maximum context length is \d+ tokens/i,
|
|
10
|
+
/exceeds the limit of \d+/i,
|
|
11
|
+
/exceeds the available context size/i,
|
|
12
|
+
/greater than the context length/i,
|
|
13
|
+
/context window exceeds limit/i,
|
|
14
|
+
/exceeded model token limit/i,
|
|
15
|
+
/context[_ ]length[_ ]exceeded/i,
|
|
16
|
+
/request entity too large/i,
|
|
17
|
+
/context length is only \d+ tokens/i,
|
|
18
|
+
/input length.*exceeds.*context length/i,
|
|
19
|
+
/prompt too long; exceeded (?:max )?context length/i,
|
|
20
|
+
/too large for model with \d+ maximum context length/i,
|
|
21
|
+
/model_context_window_exceeded/i,
|
|
22
|
+
];
|
|
23
|
+
/** True when a provider error message indicates the input exceeded the context window. */
|
|
24
|
+
const isContextOverflow = (message) => patterns.some((pattern) => pattern.test(message)) ||
|
|
25
|
+
/^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message);
|
|
26
|
+
/** True when a thrown/produced value represents a context-overflow failure. */
|
|
27
|
+
const isContextOverflowFailure = (failure) => {
|
|
28
|
+
if (!isLLMError(failure))
|
|
29
|
+
return false;
|
|
30
|
+
const reason = failure.reason;
|
|
31
|
+
return reason._tag === 'InvalidRequest' && reason.classification === 'context-overflow';
|
|
32
|
+
};
|
|
33
|
+
export { isContextOverflow, isContextOverflowFailure };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ChatMessage, ChatRequest, StreamChunk } from '../shared/types/llm.js';
|
|
2
|
+
import type { Registry } from './registry.js';
|
|
3
|
+
import type { FallbackChain } from './routing.js';
|
|
4
|
+
import type { StreamEvent } from './schema/events.js';
|
|
5
|
+
import type { InternalRequest, Message } from './schema/messages.js';
|
|
6
|
+
type ProviderContext = {
|
|
7
|
+
registry: Registry;
|
|
8
|
+
fetchImpl?: typeof fetch;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
};
|
|
11
|
+
/** Map a shared ChatMessage to the internal Message shape. */
|
|
12
|
+
declare const toInternalMessage: (msg: ChatMessage) => Message;
|
|
13
|
+
/** Map a rich StreamEvent to the shared, agent-facing StreamChunk. */
|
|
14
|
+
declare const toStreamChunk: (event: StreamEvent) => StreamChunk | null;
|
|
15
|
+
type ChatOptions = {
|
|
16
|
+
provider: string;
|
|
17
|
+
model: string;
|
|
18
|
+
fallback?: FallbackChain;
|
|
19
|
+
/** Override sleep for retry testing. */
|
|
20
|
+
sleep?: (ms: number) => Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
declare const buildInternalRequest: (request: ChatRequest, provider: string, modelId: string) => InternalRequest;
|
|
23
|
+
/**
|
|
24
|
+
* Stream a chat request incrementally as agent-facing StreamChunk values.
|
|
25
|
+
* Retries/falls over only BEFORE the first chunk is yielded; once streaming
|
|
26
|
+
* begins, errors propagate to the caller (per spec §7.6 fall-over policy).
|
|
27
|
+
*/
|
|
28
|
+
declare const chatStream: (ctx: ProviderContext, request: ChatRequest, options: ChatOptions) => AsyncGenerator<StreamChunk, void, unknown>;
|
|
29
|
+
/** Non-streaming chat: consume the stream and return the final text. */
|
|
30
|
+
declare const chat: (ctx: ProviderContext, request: ChatRequest, options: ChatOptions) => Promise<string>;
|
|
31
|
+
export type { ChatOptions, ProviderContext };
|
|
32
|
+
export { buildInternalRequest, chat, chatStream, toInternalMessage, toStreamChunk };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { bodyFrom, finalizeStream, initialStepState, parseChunk, step, } from './protocols/openai-compat.js';
|
|
2
|
+
import { resolveRoute } from './registry.js';
|
|
3
|
+
import { withRetry } from './retry.js';
|
|
4
|
+
import { shouldFallOver } from './routing.js';
|
|
5
|
+
import { model as makeModel } from './schema/options.js';
|
|
6
|
+
import { httpPost, sseFraming } from './transport.js';
|
|
7
|
+
const safeParseArgs = (raw) => {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(raw);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return raw;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/** Map a shared ChatMessage to the internal Message shape. */
|
|
16
|
+
const toInternalMessage = (msg) => {
|
|
17
|
+
// tool role messages carry tool result text — MUST handle before the
|
|
18
|
+
// string-content early return below, because context.ts serialises tool
|
|
19
|
+
// result output to a JSON string (msg.content is a string for tool msgs).
|
|
20
|
+
// If this runs after the early return, toolCallId is lost and the provider
|
|
21
|
+
// receives a tool message without tool_call_id → "invalid tool_call_id".
|
|
22
|
+
if (msg.role === 'tool' && msg.toolCallId !== undefined) {
|
|
23
|
+
const text = typeof msg.content === 'string'
|
|
24
|
+
? msg.content
|
|
25
|
+
: msg.content.map((p) => (p.type === 'text' ? p.text : '')).join('');
|
|
26
|
+
return {
|
|
27
|
+
role: 'tool',
|
|
28
|
+
content: [
|
|
29
|
+
{
|
|
30
|
+
type: 'tool-result',
|
|
31
|
+
id: msg.toolCallId,
|
|
32
|
+
name: 'tool',
|
|
33
|
+
result: { type: 'text', value: text },
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (typeof msg.content === 'string') {
|
|
39
|
+
return { role: msg.role, content: [{ type: 'text', text: msg.content }] };
|
|
40
|
+
}
|
|
41
|
+
const parts = msg.content.map((p) => {
|
|
42
|
+
if (p.type === 'text')
|
|
43
|
+
return { type: 'text', text: p.text };
|
|
44
|
+
return { type: 'text', text: `[image: ${p.mediaType}]` };
|
|
45
|
+
});
|
|
46
|
+
// assistant messages may carry tool_calls
|
|
47
|
+
if (msg.role === 'assistant' && msg.toolCalls !== undefined) {
|
|
48
|
+
const toolParts = msg.toolCalls.map((tc) => ({
|
|
49
|
+
type: 'tool-call',
|
|
50
|
+
id: tc.id,
|
|
51
|
+
name: tc.name,
|
|
52
|
+
input: safeParseArgs(tc.arguments),
|
|
53
|
+
}));
|
|
54
|
+
return { role: 'assistant', content: [...parts, ...toolParts] };
|
|
55
|
+
}
|
|
56
|
+
return { role: msg.role, content: parts };
|
|
57
|
+
};
|
|
58
|
+
const toInternalTool = (tool) => ({
|
|
59
|
+
name: tool.name,
|
|
60
|
+
description: tool.description,
|
|
61
|
+
inputSchema: tool.parameters,
|
|
62
|
+
});
|
|
63
|
+
/** Map a rich StreamEvent to the shared, agent-facing StreamChunk. */
|
|
64
|
+
const toStreamChunk = (event) => {
|
|
65
|
+
switch (event.type) {
|
|
66
|
+
case 'text-delta':
|
|
67
|
+
return { _tag: 'text', text: event.text };
|
|
68
|
+
case 'tool-input-start':
|
|
69
|
+
return { _tag: 'tool_call_start', id: event.id, name: event.name };
|
|
70
|
+
case 'tool-input-delta':
|
|
71
|
+
return { _tag: 'tool_call_delta', id: event.id, argumentsDelta: event.text };
|
|
72
|
+
case 'tool-input-end':
|
|
73
|
+
// Redundant: the finalized `tool-call` event below carries the complete
|
|
74
|
+
// parsed input and maps to tool_call_end. Emitting both would duplicate
|
|
75
|
+
// the end signal per tool call.
|
|
76
|
+
return null;
|
|
77
|
+
case 'tool-call':
|
|
78
|
+
return { _tag: 'tool_call_end', id: event.id, argumentsFinal: JSON.stringify(event.input) };
|
|
79
|
+
case 'reasoning-delta':
|
|
80
|
+
return { _tag: 'thinking', text: event.text };
|
|
81
|
+
case 'step-finish':
|
|
82
|
+
if (event.usage !== undefined) {
|
|
83
|
+
return {
|
|
84
|
+
_tag: 'usage',
|
|
85
|
+
inputTokens: event.usage.inputTokens ?? 0,
|
|
86
|
+
outputTokens: event.usage.outputTokens ?? 0,
|
|
87
|
+
cacheRead: event.usage.cacheReadInputTokens,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
case 'finish':
|
|
92
|
+
return { _tag: 'done', finishReason: event.reason };
|
|
93
|
+
case 'provider-error':
|
|
94
|
+
return {
|
|
95
|
+
_tag: 'error',
|
|
96
|
+
error: { message: event.message, retryable: event.retryable ?? false },
|
|
97
|
+
};
|
|
98
|
+
default:
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const buildInternalRequest = (request, provider, modelId) => ({
|
|
103
|
+
model: makeModel(modelId, provider),
|
|
104
|
+
system: request.system !== undefined ? [{ type: 'text', text: request.system }] : [],
|
|
105
|
+
messages: request.messages.map(toInternalMessage),
|
|
106
|
+
tools: (request.tools ?? []).map(toInternalTool),
|
|
107
|
+
generation: {
|
|
108
|
+
...(request.maxTokens !== undefined ? { maxTokens: request.maxTokens } : {}),
|
|
109
|
+
...(request.temperature !== undefined ? { temperature: request.temperature } : {}),
|
|
110
|
+
},
|
|
111
|
+
toolChoice: request.tools !== undefined && request.tools.length > 0 ? { type: 'auto' } : undefined,
|
|
112
|
+
});
|
|
113
|
+
const resolveChain = (options) => options.fallback ?? {
|
|
114
|
+
primary: { provider: options.provider, model: options.model },
|
|
115
|
+
fallbacks: [],
|
|
116
|
+
maxRetries: 3,
|
|
117
|
+
retryDelay: 2000,
|
|
118
|
+
sleep: options.sleep,
|
|
119
|
+
};
|
|
120
|
+
/** Open a (retryable) connection to one target route and stream its StreamChunks. */
|
|
121
|
+
const streamTarget = async function* (ctx, request, target, chain) {
|
|
122
|
+
const resolved = resolveRoute(ctx.registry, target.provider, target.model);
|
|
123
|
+
const internal = buildInternalRequest(request, target.provider, target.model);
|
|
124
|
+
const body = bodyFrom(internal);
|
|
125
|
+
const url = `${resolved.route.baseURL}${resolved.route.path}`;
|
|
126
|
+
const authHeader = resolved.route.auth.type === 'bearer' ? resolved.route.auth.apiKey : '';
|
|
127
|
+
// Retry only the connection (the fetch). Once streaming begins, errors propagate.
|
|
128
|
+
const stream = await withRetry(() => httpPost({
|
|
129
|
+
url,
|
|
130
|
+
body,
|
|
131
|
+
headers: { authorization: `Bearer ${authHeader}`, ...resolved.route.headers() },
|
|
132
|
+
signal: ctx.signal,
|
|
133
|
+
fetchImpl: ctx.fetchImpl,
|
|
134
|
+
}), { maxRetries: chain.maxRetries, sleep: chain.sleep });
|
|
135
|
+
let state = initialStepState();
|
|
136
|
+
for await (const frame of sseFraming(stream)) {
|
|
137
|
+
const chunk = parseChunk(resolved.route.id, frame);
|
|
138
|
+
const result = step(state, chunk);
|
|
139
|
+
state = result.state;
|
|
140
|
+
for (const event of result.events) {
|
|
141
|
+
const sc = toStreamChunk(event);
|
|
142
|
+
if (sc !== null)
|
|
143
|
+
yield sc;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Stream ended without a trailing usage chunk — finalize (no-op if already finished).
|
|
147
|
+
for (const event of finalizeStream(state).events) {
|
|
148
|
+
const sc = toStreamChunk(event);
|
|
149
|
+
if (sc !== null)
|
|
150
|
+
yield sc;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Stream a chat request incrementally as agent-facing StreamChunk values.
|
|
155
|
+
* Retries/falls over only BEFORE the first chunk is yielded; once streaming
|
|
156
|
+
* begins, errors propagate to the caller (per spec §7.6 fall-over policy).
|
|
157
|
+
*/
|
|
158
|
+
const chatStream = async function* (ctx, request, options) {
|
|
159
|
+
const chain = resolveChain(options);
|
|
160
|
+
const targets = [chain.primary, ...chain.fallbacks];
|
|
161
|
+
let started = false;
|
|
162
|
+
let lastError;
|
|
163
|
+
for (const target of targets) {
|
|
164
|
+
if (target === undefined)
|
|
165
|
+
continue;
|
|
166
|
+
resolveRoute(ctx.registry, target.provider, target.model); // fail-fast NoRoute
|
|
167
|
+
try {
|
|
168
|
+
for await (const sc of streamTarget(ctx, request, target, chain)) {
|
|
169
|
+
started = true;
|
|
170
|
+
yield sc;
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
lastError = error;
|
|
176
|
+
if (started || !shouldFallOver(error))
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
throw lastError;
|
|
181
|
+
};
|
|
182
|
+
/** Non-streaming chat: consume the stream and return the final text. */
|
|
183
|
+
const chat = async (ctx, request, options) => {
|
|
184
|
+
let text = '';
|
|
185
|
+
for await (const chunk of chatStream(ctx, request, options)) {
|
|
186
|
+
if (chunk._tag === 'text')
|
|
187
|
+
text += chunk.text;
|
|
188
|
+
}
|
|
189
|
+
return text;
|
|
190
|
+
};
|
|
191
|
+
export { buildInternalRequest, chat, chatStream, toInternalMessage, toStreamChunk };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ModelCapabilities, ModelRole } from '../shared/types/llm.js';
|
|
2
|
+
import { openAICompatRoute } from './protocols/openai-compat.js';
|
|
3
|
+
import type { Model } from './schema/options.js';
|
|
4
|
+
type RouteEntry = ReturnType<typeof openAICompatRoute> & {
|
|
5
|
+
models: Record<string, ModelCapabilities>;
|
|
6
|
+
};
|
|
7
|
+
type Registry = {
|
|
8
|
+
routes: Map<string, RouteEntry>;
|
|
9
|
+
roles: Map<string, {
|
|
10
|
+
provider: string;
|
|
11
|
+
model: string;
|
|
12
|
+
}>;
|
|
13
|
+
};
|
|
14
|
+
declare const createRegistry: () => Registry;
|
|
15
|
+
type ProviderInput = {
|
|
16
|
+
name: string;
|
|
17
|
+
baseURL: string;
|
|
18
|
+
apiKey: string;
|
|
19
|
+
headers?: () => Record<string, string>;
|
|
20
|
+
path?: string;
|
|
21
|
+
models?: Record<string, ModelCapabilities>;
|
|
22
|
+
};
|
|
23
|
+
declare const registerProvider: (registry: Registry, input: ProviderInput) => void;
|
|
24
|
+
type ResolveResult = {
|
|
25
|
+
route: RouteEntry;
|
|
26
|
+
model: Model;
|
|
27
|
+
capabilities: ModelCapabilities;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a (provider, modelId) pair into a route + typed model.
|
|
31
|
+
* Throws NoRoute when the provider is unknown.
|
|
32
|
+
*/
|
|
33
|
+
declare const resolveRoute: (registry: Registry, provider: string, modelId: string) => ResolveResult;
|
|
34
|
+
/** Resolve the (provider, model) configured for a given role. */
|
|
35
|
+
declare const resolveModelByRole: (registry: Registry, role: ModelRole) => {
|
|
36
|
+
provider: string;
|
|
37
|
+
model: string;
|
|
38
|
+
};
|
|
39
|
+
/** Bind a role to a (provider, model) pair. */
|
|
40
|
+
declare const setRole: (registry: Registry, role: ModelRole, provider: string, model: string) => void;
|
|
41
|
+
/** Default role + a starter catalog of well-known models. */
|
|
42
|
+
declare const builtinCapabilities: Record<string, Record<string, ModelCapabilities>>;
|
|
43
|
+
export type { ProviderInput, Registry, ResolveResult, RouteEntry };
|
|
44
|
+
export { builtinCapabilities, createRegistry, registerProvider, resolveModelByRole, resolveRoute, setRole, };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { openAICompatRoute } from './protocols/openai-compat.js';
|
|
2
|
+
import { llmError } from './schema/errors.js';
|
|
3
|
+
import { model as makeModel } from './schema/options.js';
|
|
4
|
+
const createRegistry = () => ({ routes: new Map(), roles: new Map() });
|
|
5
|
+
const registerProvider = (registry, input) => {
|
|
6
|
+
registry.routes.set(input.name, {
|
|
7
|
+
...openAICompatRoute({
|
|
8
|
+
id: input.name,
|
|
9
|
+
provider: input.name,
|
|
10
|
+
baseURL: input.baseURL,
|
|
11
|
+
apiKey: input.apiKey,
|
|
12
|
+
headers: input.headers,
|
|
13
|
+
path: input.path,
|
|
14
|
+
}),
|
|
15
|
+
models: input.models ?? {},
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a (provider, modelId) pair into a route + typed model.
|
|
20
|
+
* Throws NoRoute when the provider is unknown.
|
|
21
|
+
*/
|
|
22
|
+
const resolveRoute = (registry, provider, modelId) => {
|
|
23
|
+
const route = registry.routes.get(provider);
|
|
24
|
+
if (route === undefined) {
|
|
25
|
+
throw llmError('LLM', 'resolve', {
|
|
26
|
+
_tag: 'NoRoute',
|
|
27
|
+
route: `${provider}`,
|
|
28
|
+
provider,
|
|
29
|
+
model: modelId,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const capabilities = route.models[modelId] ??
|
|
33
|
+
{
|
|
34
|
+
contextWindow: 8192,
|
|
35
|
+
maxOutput: 4096,
|
|
36
|
+
supportsTools: true,
|
|
37
|
+
supportsVision: false,
|
|
38
|
+
supportsThinking: false,
|
|
39
|
+
costPer1kInput: 0,
|
|
40
|
+
costPer1kOutput: 0,
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
route,
|
|
44
|
+
model: makeModel(modelId, provider, {
|
|
45
|
+
context: capabilities.contextWindow,
|
|
46
|
+
output: capabilities.maxOutput,
|
|
47
|
+
}),
|
|
48
|
+
capabilities,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
/** Resolve the (provider, model) configured for a given role. */
|
|
52
|
+
const resolveModelByRole = (registry, role) => {
|
|
53
|
+
const key = role._tag;
|
|
54
|
+
const entry = registry.roles.get(key);
|
|
55
|
+
if (entry === undefined) {
|
|
56
|
+
const fallback = registry.roles.get('default');
|
|
57
|
+
if (fallback === undefined) {
|
|
58
|
+
throw llmError('LLM', 'resolve', {
|
|
59
|
+
_tag: 'NoRoute',
|
|
60
|
+
route: `role:${key}`,
|
|
61
|
+
provider: 'unknown',
|
|
62
|
+
model: 'unknown',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return fallback;
|
|
66
|
+
}
|
|
67
|
+
return entry;
|
|
68
|
+
};
|
|
69
|
+
/** Bind a role to a (provider, model) pair. */
|
|
70
|
+
const setRole = (registry, role, provider, model) => {
|
|
71
|
+
registry.roles.set(role._tag, { provider, model });
|
|
72
|
+
};
|
|
73
|
+
/** Default role + a starter catalog of well-known models. */
|
|
74
|
+
const builtinCapabilities = {
|
|
75
|
+
openai: {
|
|
76
|
+
'gpt-4o': {
|
|
77
|
+
contextWindow: 128000,
|
|
78
|
+
maxOutput: 16384,
|
|
79
|
+
supportsTools: true,
|
|
80
|
+
supportsVision: true,
|
|
81
|
+
supportsThinking: false,
|
|
82
|
+
costPer1kInput: 0.0025,
|
|
83
|
+
costPer1kOutput: 0.01,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
deepseek: {
|
|
87
|
+
'deepseek-chat': {
|
|
88
|
+
contextWindow: 64000,
|
|
89
|
+
maxOutput: 8192,
|
|
90
|
+
supportsTools: true,
|
|
91
|
+
supportsVision: false,
|
|
92
|
+
supportsThinking: false,
|
|
93
|
+
costPer1kInput: 0.00014,
|
|
94
|
+
costPer1kOutput: 0.00028,
|
|
95
|
+
},
|
|
96
|
+
'deepseek-reasoner': {
|
|
97
|
+
contextWindow: 64000,
|
|
98
|
+
maxOutput: 8192,
|
|
99
|
+
supportsTools: false,
|
|
100
|
+
supportsVision: false,
|
|
101
|
+
supportsThinking: true,
|
|
102
|
+
costPer1kInput: 0.00055,
|
|
103
|
+
costPer1kOutput: 0.00219,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
export { builtinCapabilities, createRegistry, registerProvider, resolveModelByRole, resolveRoute, setRole, };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { LLMErrorReason } from './schema/errors.js';
|
|
2
|
+
declare const RETRY_INITIAL_DELAY = 2000;
|
|
3
|
+
declare const RETRY_MAX_DELAY_NO_HEADERS = 30000;
|
|
4
|
+
declare const RETRY_MAX_DELAY = 2147483647;
|
|
5
|
+
/**
|
|
6
|
+
* Compute the delay before the next retry attempt (ms).
|
|
7
|
+
* Honors retry-after / retry-after-ms headers when present, else exponential backoff.
|
|
8
|
+
*/
|
|
9
|
+
declare const delay: (attempt: number, error?: unknown) => number;
|
|
10
|
+
/** A normalized, retryable error descriptor for the session layer. */
|
|
11
|
+
type Retryable = {
|
|
12
|
+
message: string;
|
|
13
|
+
reason: LLMErrorReason;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Decide whether a thrown error is retryable. Returns undefined when not retryable
|
|
17
|
+
* (e.g. context overflow, auth, invalid request).
|
|
18
|
+
*/
|
|
19
|
+
declare const retryable: (error: unknown) => Retryable | undefined;
|
|
20
|
+
type RetryOptions = {
|
|
21
|
+
maxRetries: number;
|
|
22
|
+
/** Override sleep for testing. Defaults to setTimeout-based sleep. */
|
|
23
|
+
sleep?: (ms: number) => Promise<void>;
|
|
24
|
+
/** Called with each retry attempt metadata. */
|
|
25
|
+
onRetry?: (info: {
|
|
26
|
+
attempt: number;
|
|
27
|
+
delayMs: number;
|
|
28
|
+
error: unknown;
|
|
29
|
+
}) => void;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Run an async operation with retry. Retries only on retryable LLM errors.
|
|
33
|
+
* Non-retryable errors (including context overflow) are re-thrown immediately.
|
|
34
|
+
*/
|
|
35
|
+
declare const withRetry: <T>(fn: () => Promise<T>, options: RetryOptions) => Promise<T>;
|
|
36
|
+
export type { Retryable, RetryOptions };
|
|
37
|
+
export { delay, RETRY_INITIAL_DELAY, RETRY_MAX_DELAY, RETRY_MAX_DELAY_NO_HEADERS, retryable, withRetry, };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { isLLMError, reasonRetryAfterMs, reasonRetryable } from './schema/errors.js';
|
|
2
|
+
const RETRY_INITIAL_DELAY = 2_000;
|
|
3
|
+
const RETRY_BACKOFF_FACTOR = 2;
|
|
4
|
+
const RETRY_MAX_DELAY_NO_HEADERS = 30_000;
|
|
5
|
+
const RETRY_MAX_DELAY = 2_147_483_647;
|
|
6
|
+
/** Extract response headers from an LLMError's http context (if any). */
|
|
7
|
+
const errorHeaders = (error) => {
|
|
8
|
+
if (!isLLMError(error))
|
|
9
|
+
return undefined;
|
|
10
|
+
const reason = error.reason;
|
|
11
|
+
if ('http' in reason && reason.http?.response) {
|
|
12
|
+
return reason.http.response.headers;
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
};
|
|
16
|
+
/** Cap a delay to the 32-bit safe ceiling. */
|
|
17
|
+
const capDelay = (ms) => Math.min(ms, RETRY_MAX_DELAY);
|
|
18
|
+
/**
|
|
19
|
+
* Compute the delay before the next retry attempt (ms).
|
|
20
|
+
* Honors retry-after / retry-after-ms headers when present, else exponential backoff.
|
|
21
|
+
*/
|
|
22
|
+
const delay = (attempt, error) => {
|
|
23
|
+
const headers = errorHeaders(error);
|
|
24
|
+
if (headers) {
|
|
25
|
+
const retryAfterMs = headers['retry-after-ms'];
|
|
26
|
+
if (retryAfterMs !== undefined) {
|
|
27
|
+
const parsedMs = Number.parseFloat(retryAfterMs);
|
|
28
|
+
if (!Number.isNaN(parsedMs))
|
|
29
|
+
return capDelay(parsedMs);
|
|
30
|
+
}
|
|
31
|
+
const retryAfter = headers['retry-after'];
|
|
32
|
+
if (retryAfter !== undefined) {
|
|
33
|
+
const parsedSeconds = Number.parseFloat(retryAfter);
|
|
34
|
+
if (!Number.isNaN(parsedSeconds))
|
|
35
|
+
return capDelay(Math.ceil(parsedSeconds * 1000));
|
|
36
|
+
const parsed = Date.parse(retryAfter) - Date.now();
|
|
37
|
+
if (!Number.isNaN(parsed) && parsed > 0)
|
|
38
|
+
return capDelay(Math.ceil(parsed));
|
|
39
|
+
}
|
|
40
|
+
return capDelay(RETRY_INITIAL_DELAY * RETRY_BACKOFF_FACTOR ** (attempt - 1));
|
|
41
|
+
}
|
|
42
|
+
return capDelay(Math.min(RETRY_INITIAL_DELAY * RETRY_BACKOFF_FACTOR ** (attempt - 1), RETRY_MAX_DELAY_NO_HEADERS));
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Decide whether a thrown error is retryable. Returns undefined when not retryable
|
|
46
|
+
* (e.g. context overflow, auth, invalid request).
|
|
47
|
+
*/
|
|
48
|
+
const retryable = (error) => {
|
|
49
|
+
if (!isLLMError(error))
|
|
50
|
+
return undefined;
|
|
51
|
+
const reason = error.reason;
|
|
52
|
+
if (!reasonRetryable(reason))
|
|
53
|
+
return undefined;
|
|
54
|
+
return { message: error.message, reason };
|
|
55
|
+
};
|
|
56
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
57
|
+
/**
|
|
58
|
+
* Run an async operation with retry. Retries only on retryable LLM errors.
|
|
59
|
+
* Non-retryable errors (including context overflow) are re-thrown immediately.
|
|
60
|
+
*/
|
|
61
|
+
const withRetry = async (fn, options) => {
|
|
62
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
63
|
+
let attempt = 0;
|
|
64
|
+
for (;;) {
|
|
65
|
+
try {
|
|
66
|
+
return await fn();
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const canRetry = retryable(error);
|
|
70
|
+
if (!canRetry || attempt >= options.maxRetries)
|
|
71
|
+
throw error;
|
|
72
|
+
attempt += 1;
|
|
73
|
+
const fallbackReason = isLLMError(error)
|
|
74
|
+
? error.reason
|
|
75
|
+
: { _tag: 'InvalidRequest', message: '' };
|
|
76
|
+
const delayMs = reasonRetryAfterMs(fallbackReason) ?? delay(attempt, error);
|
|
77
|
+
options.onRetry?.({ attempt, delayMs, error });
|
|
78
|
+
await sleep(delayMs);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
export { delay, RETRY_INITIAL_DELAY, RETRY_MAX_DELAY, RETRY_MAX_DELAY_NO_HEADERS, retryable, withRetry, };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Registry } from './registry.js';
|
|
2
|
+
type FallbackChain = {
|
|
3
|
+
primary: {
|
|
4
|
+
provider: string;
|
|
5
|
+
model: string;
|
|
6
|
+
};
|
|
7
|
+
fallbacks: {
|
|
8
|
+
provider: string;
|
|
9
|
+
model: string;
|
|
10
|
+
}[];
|
|
11
|
+
maxRetries: number;
|
|
12
|
+
retryDelay: number;
|
|
13
|
+
/** Override sleep for testing. */
|
|
14
|
+
sleep?: (ms: number) => Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
type RunFn<T> = (provider: string, model: string) => Promise<T>;
|
|
17
|
+
/**
|
|
18
|
+
* Run `run` against the primary route first. If it throws a retryable error
|
|
19
|
+
* that exhausts retries, try each fallback in order. Non-retryable errors
|
|
20
|
+
* (context overflow, auth, invalid request) propagate immediately without
|
|
21
|
+
* trying fallbacks.
|
|
22
|
+
*/
|
|
23
|
+
declare const runWithFallback: <T>(registry: Registry, chain: FallbackChain, run: RunFn<T>) => Promise<{
|
|
24
|
+
result: T;
|
|
25
|
+
provider: string;
|
|
26
|
+
model: string;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Whether an error should trigger a fallback after retries are exhausted.
|
|
30
|
+
* Per spec §7.6: RateLimit (after retries), ProviderInternal, and Authentication
|
|
31
|
+
* fall over to the next route. Context-overflow, invalid request, and transport
|
|
32
|
+
* errors propagate without trying fallbacks.
|
|
33
|
+
*/
|
|
34
|
+
declare const shouldFallOver: (error: unknown) => boolean;
|
|
35
|
+
export type { FallbackChain };
|
|
36
|
+
export { runWithFallback, shouldFallOver };
|