@datename/fastcode-termux 0.1.6 → 0.1.8-beta.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 +4 -0
- package/README.md +67 -16
- package/dist/agent/approval-json.d.ts +43 -0
- package/dist/agent/approval-json.js +69 -0
- package/dist/agent/approval-native.d.ts +3 -0
- package/dist/agent/approval-native.js +165 -0
- package/dist/agent/approval-store.d.ts +16 -0
- package/dist/agent/approval-store.js +21 -0
- package/dist/agent/approval.d.ts +83 -0
- package/dist/agent/approval.js +42 -0
- package/dist/agent/compaction.d.ts +41 -0
- package/dist/agent/compaction.js +270 -0
- package/dist/agent/context.d.ts +29 -0
- package/dist/agent/context.js +62 -0
- package/dist/agent/guards.d.ts +24 -16
- package/dist/agent/guards.js +1167 -18
- package/dist/agent/ignore.d.ts +11 -0
- package/dist/agent/ignore.js +129 -0
- package/dist/agent/policy/defaults.d.ts +16 -0
- package/dist/agent/policy/defaults.js +17 -0
- package/dist/agent/policy/egress.d.ts +32 -0
- package/dist/agent/policy/egress.js +280 -0
- package/dist/agent/process-tree.d.ts +22 -0
- package/dist/agent/process-tree.js +102 -0
- package/dist/agent/protocol-mode.d.ts +6 -0
- package/dist/agent/protocol-mode.js +51 -0
- package/dist/agent/protocol.d.ts +20 -1
- package/dist/agent/protocol.js +169 -39
- package/dist/agent/registry.d.ts +19 -0
- package/dist/agent/registry.js +247 -0
- package/dist/agent/runner.d.ts +127 -0
- package/dist/agent/runner.js +391 -0
- package/dist/agent/shell-ast.d.ts +11 -0
- package/dist/agent/shell-ast.js +204 -0
- package/dist/agent/shell-session.d.ts +51 -0
- package/dist/agent/shell-session.js +611 -0
- package/dist/agent/stream.d.ts +29 -0
- package/dist/agent/stream.js +103 -0
- package/dist/agent/tool-schemas.d.ts +2 -0
- package/dist/agent/tool-schemas.js +82 -0
- package/dist/agent/tools/append_file.d.ts +2 -0
- package/dist/agent/tools/append_file.js +28 -0
- package/dist/agent/tools/apply_patch.d.ts +10 -0
- package/dist/agent/tools/apply_patch.js +92 -0
- package/dist/agent/tools/copy_file.d.ts +2 -0
- package/dist/agent/tools/copy_file.js +53 -0
- package/dist/agent/tools/delete_file.d.ts +2 -0
- package/dist/agent/tools/delete_file.js +43 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +54 -0
- package/dist/agent/tools/index.d.ts +2 -0
- package/dist/agent/tools/index.js +32 -0
- package/dist/agent/tools/list_files.d.ts +2 -0
- package/dist/agent/tools/list_files.js +17 -0
- package/dist/agent/tools/make_dir.d.ts +2 -0
- package/dist/agent/tools/make_dir.js +26 -0
- package/dist/agent/tools/move_path.d.ts +9 -0
- package/dist/agent/tools/move_path.js +73 -0
- package/dist/agent/tools/paths.d.ts +7 -0
- package/dist/agent/tools/paths.js +84 -0
- package/dist/agent/tools/pwd.d.ts +2 -0
- package/dist/agent/tools/pwd.js +10 -0
- package/dist/agent/tools/read_file.d.ts +2 -0
- package/dist/agent/tools/read_file.js +81 -0
- package/dist/agent/tools/read_many.d.ts +2 -0
- package/dist/agent/tools/read_many.js +43 -0
- package/dist/agent/tools/readers.d.ts +58 -0
- package/dist/agent/tools/readers.js +426 -0
- package/dist/agent/tools/replace_in_file.d.ts +2 -0
- package/dist/agent/tools/replace_in_file.js +63 -0
- package/dist/agent/tools/shell.d.ts +2 -0
- package/dist/agent/tools/shell.js +48 -0
- package/dist/agent/tools/stat_path.d.ts +2 -0
- package/dist/agent/tools/stat_path.js +31 -0
- package/dist/agent/tools/types.d.ts +65 -0
- package/dist/agent/tools/types.js +1 -0
- package/dist/agent/tools/validate.d.ts +19 -0
- package/dist/agent/tools/validate.js +92 -0
- package/dist/agent/tools/write_file.d.ts +2 -0
- package/dist/agent/tools/write_file.js +28 -0
- package/dist/agent/tools.d.ts +3 -1
- package/dist/agent/tools.js +2 -377
- package/dist/app.d.ts +2 -1
- package/dist/app.js +846 -173
- package/dist/cli-main.js +363 -0
- package/dist/cli.js +444 -333
- package/dist/commands/about.js +1 -1
- package/dist/commands/attach.d.ts +9 -0
- package/dist/commands/attach.js +15 -0
- package/dist/commands/bg.d.ts +7 -0
- package/dist/commands/bg.js +9 -0
- package/dist/commands/cancel.d.ts +10 -0
- package/dist/commands/cancel.js +10 -0
- package/dist/commands/clear.d.ts +1 -0
- package/dist/commands/clear.js +1 -1
- package/dist/commands/compact.d.ts +22 -0
- package/dist/commands/compact.js +34 -0
- package/dist/commands/context.d.ts +7 -0
- package/dist/commands/context.js +25 -0
- package/dist/commands/edit.d.ts +10 -0
- package/dist/commands/edit.js +36 -0
- package/dist/commands/effort.d.ts +25 -0
- package/dist/commands/effort.js +48 -0
- package/dist/commands/expand.d.ts +10 -0
- package/dist/commands/expand.js +8 -0
- package/dist/commands/index.js +24 -0
- package/dist/commands/jobs.d.ts +7 -0
- package/dist/commands/jobs.js +17 -0
- package/dist/commands/kill.d.ts +7 -0
- package/dist/commands/kill.js +13 -0
- package/dist/commands/login.d.ts +6 -4
- package/dist/commands/login.js +24 -22
- package/dist/commands/logout.js +8 -16
- package/dist/commands/model.d.ts +1 -0
- package/dist/commands/model.js +10 -22
- package/dist/commands/provider.d.ts +1 -2
- package/dist/commands/provider.js +4 -11
- package/dist/commands/skills.js +34 -4
- package/dist/commands/steer.d.ts +11 -0
- package/dist/commands/steer.js +13 -0
- package/dist/commands/threads.d.ts +9 -0
- package/dist/commands/threads.js +26 -0
- package/dist/commands/whoami.js +6 -5
- package/dist/config/store.d.ts +53 -12
- package/dist/config/store.js +163 -73
- package/dist/config/trust.d.ts +30 -8
- package/dist/config/trust.js +139 -14
- package/dist/headless.d.ts +7 -0
- package/dist/headless.js +213 -0
- package/dist/index.js +19 -3
- package/dist/lib/debug.d.ts +2 -0
- package/dist/lib/debug.js +22 -0
- package/dist/lib/diagnostics.d.ts +48 -4
- package/dist/lib/diagnostics.js +90 -8
- package/dist/lib/highlight-subset.d.ts +21 -0
- package/dist/lib/highlight-subset.js +88 -0
- package/dist/lib/http.d.ts +11 -1
- package/dist/lib/http.js +62 -4
- package/dist/lib/lifecycle.d.ts +5 -0
- package/dist/lib/lifecycle.js +62 -0
- package/dist/lib/markdown.js +22 -4
- package/dist/lib/model-meta.d.ts +19 -0
- package/dist/lib/model-meta.js +113 -0
- package/dist/lib/native-tools.d.ts +12 -0
- package/dist/lib/native-tools.js +21 -0
- package/dist/lib/net.d.ts +33 -0
- package/dist/lib/net.js +168 -0
- package/dist/patch/apply.d.ts +15 -0
- package/dist/patch/apply.js +172 -0
- package/dist/patch/parse.d.ts +29 -0
- package/dist/patch/parse.js +112 -0
- package/dist/patch/plan.d.ts +49 -0
- package/dist/patch/plan.js +273 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +12 -0
- package/dist/providers/custom.d.ts +9 -0
- package/dist/providers/deepseek.d.ts +10 -0
- package/dist/providers/deepseek.js +3 -2
- package/dist/providers/google.d.ts +9 -0
- package/dist/providers/groq.d.ts +10 -0
- package/dist/providers/groq.js +2 -1
- package/dist/providers/index.d.ts +10 -201
- package/dist/providers/index.js +30 -0
- package/dist/providers/mistral.d.ts +10 -0
- package/dist/providers/mistral.js +2 -1
- package/dist/providers/moonshot.d.ts +10 -0
- package/dist/providers/moonshot.js +2 -1
- package/dist/providers/ollama.d.ts +9 -0
- package/dist/providers/openai.d.ts +10 -0
- package/dist/providers/openai.js +2 -1
- package/dist/providers/openaiCompatible.d.ts +33 -2
- package/dist/providers/openaiCompatible.js +78 -16
- package/dist/providers/openrouter.d.ts +10 -0
- package/dist/providers/openrouter.js +2 -1
- package/dist/providers/qwen.d.ts +10 -0
- package/dist/providers/qwen.js +2 -1
- package/dist/providers/spec.d.ts +12 -0
- package/dist/providers/spec.js +29 -0
- package/dist/providers/xai.d.ts +10 -0
- package/dist/providers/xai.js +2 -1
- package/dist/providers/zhipu.d.ts +10 -0
- package/dist/providers/zhipu.js +2 -1
- package/dist/session/log.d.ts +49 -14
- package/dist/session/log.js +236 -11
- package/dist/session/projection.d.ts +76 -0
- package/dist/session/projection.js +526 -0
- package/dist/skills/manager.d.ts +15 -0
- package/dist/skills/manager.js +135 -18
- package/dist/types.d.ts +75 -1
- package/dist/ui/Message.d.ts +1 -7
- package/dist/ui/Message.js +13 -2
- package/dist/ui/Picker.d.ts +8 -0
- package/dist/ui/Picker.js +24 -0
- package/dist/ui/Prompt.d.ts +1 -2
- package/dist/ui/Prompt.js +2 -2
- package/dist/ui/Spinner.js +11 -5
- package/dist/ui/StatusBar.d.ts +2 -1
- package/dist/ui/StatusBar.js +6 -5
- package/package.json +3 -2
- package/src/agent/approval-json.ts +103 -0
- package/src/agent/approval-native.ts +168 -0
- package/src/agent/approval-store.ts +38 -0
- package/src/agent/approval.ts +94 -0
- package/src/agent/compaction.ts +321 -0
- package/src/agent/context.ts +97 -0
- package/src/agent/guards.ts +1239 -19
- package/src/agent/ignore.ts +147 -0
- package/src/agent/policy/defaults.ts +34 -0
- package/src/agent/policy/egress.ts +335 -0
- package/src/agent/process-tree.ts +114 -0
- package/src/agent/protocol-mode.ts +59 -0
- package/src/agent/protocol.ts +191 -40
- package/src/agent/registry.ts +270 -0
- package/src/agent/runner.ts +486 -0
- package/src/agent/shell-ast.ts +227 -0
- package/src/agent/shell-session.ts +673 -0
- package/src/agent/stream.ts +143 -0
- package/src/agent/tool-schemas.ts +86 -0
- package/src/agent/tools/append_file.ts +30 -0
- package/src/agent/tools/apply_patch.ts +95 -0
- package/src/agent/tools/copy_file.ts +56 -0
- package/src/agent/tools/delete_file.ts +45 -0
- package/src/agent/tools/grep.ts +57 -0
- package/src/agent/tools/index.ts +34 -0
- package/src/agent/tools/list_files.ts +19 -0
- package/src/agent/tools/make_dir.ts +28 -0
- package/src/agent/tools/move_path.ts +83 -0
- package/src/agent/tools/paths.ts +92 -0
- package/src/agent/tools/pwd.ts +12 -0
- package/src/agent/tools/read_file.ts +84 -0
- package/src/agent/tools/read_many.ts +45 -0
- package/src/agent/tools/readers.ts +491 -0
- package/src/agent/tools/replace_in_file.ts +66 -0
- package/src/agent/tools/shell.ts +53 -0
- package/src/agent/tools/stat_path.ts +33 -0
- package/src/agent/tools/types.ts +84 -0
- package/src/agent/tools/validate.ts +104 -0
- package/src/agent/tools/write_file.ts +30 -0
- package/src/agent/tools.ts +3 -377
- package/src/app.ts +853 -170
- package/src/{cli.ts → cli-main.ts} +64 -9
- package/src/commands/about.ts +1 -1
- package/src/commands/attach.ts +16 -0
- package/src/commands/bg.ts +9 -0
- package/src/commands/cancel.ts +10 -0
- package/src/commands/clear.ts +1 -1
- package/src/commands/compact.ts +35 -0
- package/src/commands/context.ts +26 -0
- package/src/commands/edit.ts +37 -0
- package/src/commands/effort.ts +49 -0
- package/src/commands/expand.ts +8 -0
- package/src/commands/index.ts +24 -0
- package/src/commands/jobs.ts +18 -0
- package/src/commands/kill.ts +14 -0
- package/src/commands/login.ts +24 -22
- package/src/commands/logout.ts +8 -16
- package/src/commands/model.ts +10 -22
- package/src/commands/provider.ts +4 -11
- package/src/commands/skills.ts +34 -4
- package/src/commands/steer.ts +13 -0
- package/src/commands/threads.ts +28 -0
- package/src/commands/whoami.ts +6 -5
- package/src/config/store.ts +168 -77
- package/src/config/trust.ts +181 -22
- package/src/headless.ts +225 -0
- package/src/index.ts +19 -3
- package/src/lib/debug.ts +24 -0
- package/src/lib/diagnostics.ts +90 -8
- package/src/lib/highlight-subset.ts +93 -0
- package/src/lib/http.ts +66 -4
- package/src/lib/lifecycle.ts +69 -0
- package/src/lib/markdown.ts +22 -4
- package/src/lib/model-meta.ts +138 -0
- package/src/lib/native-tools.ts +32 -0
- package/src/lib/net.ts +204 -0
- package/src/patch/apply.ts +194 -0
- package/src/patch/parse.ts +131 -0
- package/src/patch/plan.ts +317 -0
- package/src/providers/anthropic.ts +13 -1
- package/src/providers/deepseek.ts +3 -2
- package/src/providers/groq.ts +2 -1
- package/src/providers/index.ts +32 -1
- package/src/providers/mistral.ts +2 -1
- package/src/providers/moonshot.ts +2 -1
- package/src/providers/openai.ts +2 -1
- package/src/providers/openaiCompatible.ts +104 -19
- package/src/providers/openrouter.ts +2 -1
- package/src/providers/qwen.ts +2 -1
- package/src/providers/spec.ts +42 -0
- package/src/providers/xai.ts +2 -1
- package/src/providers/zhipu.ts +2 -1
- package/src/session/log.ts +311 -29
- package/src/session/projection.ts +621 -0
- package/src/skills/manager.ts +140 -18
- package/src/types/highlight-shims.d.ts +14 -0
- package/src/types.ts +79 -2
- package/src/ui/Message.ts +13 -2
- package/src/ui/Picker.ts +24 -0
- package/src/ui/Prompt.ts +2 -2
- package/src/ui/Spinner.ts +11 -5
- package/src/ui/StatusBar.ts +6 -5
- package/tsconfig.json +1 -0
- /package/dist/{cli.d.ts → cli-main.d.ts} +0 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { closeSync, constants as fsConstants, lstatSync, openSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { sandboxPath } from "../agent/tools/paths.js";
|
|
4
|
+
import { parsePatch, type PatchHunk, type PatchOp } from "./parse.js";
|
|
5
|
+
|
|
6
|
+
export interface PlannedFile {
|
|
7
|
+
kind: "add" | "delete" | "update";
|
|
8
|
+
relPath: string;
|
|
9
|
+
path: string;
|
|
10
|
+
moveRel?: string;
|
|
11
|
+
moveTo?: string;
|
|
12
|
+
existed: boolean;
|
|
13
|
+
origMode: number;
|
|
14
|
+
eol: "\n" | "\r\n";
|
|
15
|
+
oldContent?: string;
|
|
16
|
+
newContent?: string;
|
|
17
|
+
diff: string;
|
|
18
|
+
changedLines: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PatchPlan {
|
|
22
|
+
files: PlannedFile[];
|
|
23
|
+
warnings: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type PlanResult = { ok: true; plan: PatchPlan } | { ok: false; output: string };
|
|
27
|
+
|
|
28
|
+
export type PatchReview =
|
|
29
|
+
| { kind: "allow" }
|
|
30
|
+
| { kind: "deny"; reason: string }
|
|
31
|
+
| { kind: "pending"; reason: string; changedLines: number };
|
|
32
|
+
|
|
33
|
+
export function planPatch(patch: string, cwd: string): PlanResult {
|
|
34
|
+
const parsed = parsePatch(patch);
|
|
35
|
+
if (parsed.ok === false)
|
|
36
|
+
return { ok: false, output: parsed.output };
|
|
37
|
+
const files: PlannedFile[] = [];
|
|
38
|
+
const warnings: string[] = [];
|
|
39
|
+
for (const op of parsed.ops) {
|
|
40
|
+
const planned = planOp(op, cwd, warnings);
|
|
41
|
+
if (planned.ok === false)
|
|
42
|
+
return { ok: false, output: planned.output };
|
|
43
|
+
files.push(planned.file);
|
|
44
|
+
}
|
|
45
|
+
return { ok: true, plan: { files, warnings } };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function planOp(op: PatchOp, cwd: string, warnings: string[]): { ok: true; file: PlannedFile } | { ok: false; output: string } {
|
|
49
|
+
try {
|
|
50
|
+
const path = sandboxPath(cwd, op.path);
|
|
51
|
+
if (op.kind === "add") {
|
|
52
|
+
if (lexists(path))
|
|
53
|
+
return { ok: false, output: `Add File target already exists: ${op.path}` };
|
|
54
|
+
const newContent = op.lines.length > 0 ? op.lines.join("\n") + "\n" : "";
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
file: {
|
|
58
|
+
kind: "add",
|
|
59
|
+
relPath: op.path,
|
|
60
|
+
path,
|
|
61
|
+
existed: false,
|
|
62
|
+
origMode: 0o644,
|
|
63
|
+
eol: "\n",
|
|
64
|
+
newContent,
|
|
65
|
+
diff: makeDiff(null, op.path, "", newContent, "add"),
|
|
66
|
+
changedLines: newContent.split("\n").filter(Boolean).length,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (!lexists(path))
|
|
71
|
+
return { ok: false, output: `file not found: ${op.path}` };
|
|
72
|
+
const info = lstatSync(path);
|
|
73
|
+
if (info.isSymbolicLink())
|
|
74
|
+
return { ok: false, output: `refusing to patch a symbolic link: ${op.path}` };
|
|
75
|
+
if (!info.isFile())
|
|
76
|
+
return { ok: false, output: `not a regular file: ${op.path}` };
|
|
77
|
+
const oldContent = readNoFollow(path);
|
|
78
|
+
if (op.kind === "delete") {
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
file: {
|
|
82
|
+
kind: "delete",
|
|
83
|
+
relPath: op.path,
|
|
84
|
+
path,
|
|
85
|
+
existed: true,
|
|
86
|
+
origMode: info.mode & 0o777,
|
|
87
|
+
eol: detectEol(oldContent),
|
|
88
|
+
oldContent,
|
|
89
|
+
diff: makeDiff(op.path, null, oldContent, "", "delete"),
|
|
90
|
+
changedLines: oldContent.split(/\r?\n/).length,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const eol = detectEol(oldContent);
|
|
95
|
+
if (eol === "\n" && /\r\n/.test(oldContent))
|
|
96
|
+
warnings.push(`mixed line endings in ${op.path}; writing LF`);
|
|
97
|
+
let newContent = oldContent;
|
|
98
|
+
if (op.hunks.length > 0) {
|
|
99
|
+
const applied = applyHunks(oldContent, op.hunks);
|
|
100
|
+
if (applied.ok === false)
|
|
101
|
+
return applied;
|
|
102
|
+
newContent = applied.content;
|
|
103
|
+
if (eol === "\r\n")
|
|
104
|
+
newContent = newContent.replace(/\n/g, "\r\n");
|
|
105
|
+
}
|
|
106
|
+
let moveTo: string | undefined;
|
|
107
|
+
let moveRel: string | undefined;
|
|
108
|
+
if (op.moveTo) {
|
|
109
|
+
moveTo = sandboxPath(cwd, op.moveTo);
|
|
110
|
+
if (lexists(moveTo))
|
|
111
|
+
return { ok: false, output: `Move target already exists: ${op.moveTo}` };
|
|
112
|
+
moveRel = op.moveTo;
|
|
113
|
+
}
|
|
114
|
+
const diff = makeDiff(op.path, op.moveTo ?? op.path, oldContent, newContent, "update");
|
|
115
|
+
return {
|
|
116
|
+
ok: true,
|
|
117
|
+
file: {
|
|
118
|
+
kind: "update",
|
|
119
|
+
relPath: op.path,
|
|
120
|
+
path,
|
|
121
|
+
moveRel,
|
|
122
|
+
moveTo,
|
|
123
|
+
existed: true,
|
|
124
|
+
origMode: info.mode & 0o777,
|
|
125
|
+
eol,
|
|
126
|
+
oldContent,
|
|
127
|
+
newContent,
|
|
128
|
+
diff,
|
|
129
|
+
changedLines: countChangedLines(oldContent, newContent),
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
catch (err: any) {
|
|
134
|
+
return { ok: false, output: err?.message ?? String(err) };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function reviewPatchPlan(plan: PatchPlan): PatchReview {
|
|
139
|
+
let changed = 0;
|
|
140
|
+
for (const file of plan.files) {
|
|
141
|
+
changed += file.changedLines;
|
|
142
|
+
if (isHardDenied(file.path))
|
|
143
|
+
return { kind: "deny", reason: `patch targets a protected path: ${file.relPath}` };
|
|
144
|
+
if (isSensitivePath(file.path))
|
|
145
|
+
return { kind: "pending", reason: `patch targets a sensitive path: ${file.relPath}`, changedLines: changed };
|
|
146
|
+
}
|
|
147
|
+
if (changed > 200)
|
|
148
|
+
return { kind: "pending", reason: `patch changes ${changed} lines (limit 200)`, changedLines: changed };
|
|
149
|
+
return { kind: "allow" };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isHardDenied(path: string): boolean {
|
|
153
|
+
const normalized = path.replace(/\\/g, "/").toLowerCase();
|
|
154
|
+
const segments = normalized.split("/");
|
|
155
|
+
if (segments.includes(".ssh") || segments.includes(".gnupg"))
|
|
156
|
+
return true;
|
|
157
|
+
if (normalized.includes("/.git/hooks/"))
|
|
158
|
+
return true;
|
|
159
|
+
if (normalized.includes("/android/data/"))
|
|
160
|
+
return true;
|
|
161
|
+
const name = basename(normalized);
|
|
162
|
+
return name === "auth.json" || name === "trust.json";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isSensitivePath(path: string): boolean {
|
|
166
|
+
const normalized = path.replace(/\\/g, "/").toLowerCase();
|
|
167
|
+
const name = basename(normalized);
|
|
168
|
+
return name === ".env" || name.startsWith(".env.") || /[.](pem|key|p12|pfx)$/.test(name) || normalized.includes("/.git/hooks/");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function lexists(path: string): boolean {
|
|
172
|
+
try {
|
|
173
|
+
lstatSync(path);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function readNoFollow(path: string): string {
|
|
182
|
+
const flags = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0);
|
|
183
|
+
const fd = openSync(path, flags);
|
|
184
|
+
try {
|
|
185
|
+
return readFileSync(fd, "utf8");
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
closeSync(fd);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function detectEol(content: string): "\n" | "\r\n" {
|
|
193
|
+
const crlf = (content.match(/\r\n/g) ?? []).length;
|
|
194
|
+
const lf = (content.match(/\n/g) ?? []).length;
|
|
195
|
+
return crlf > 0 && crlf === lf ? "\r\n" : "\n";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
type ApplyHunksResult = { ok: true; content: string } | { ok: false; output: string };
|
|
199
|
+
|
|
200
|
+
export function applyHunks(oldContent: string, hunks: PatchHunk[]): ApplyHunksResult {
|
|
201
|
+
const lines = oldContent.split(/\r?\n/);
|
|
202
|
+
let changed = false;
|
|
203
|
+
for (const hunk of hunks) {
|
|
204
|
+
const needle = hunk.lines.filter((line) => line.type !== "+").map((line) => line.text);
|
|
205
|
+
const replacement = hunk.lines.filter((line) => line.type !== "-").map((line) => line.text);
|
|
206
|
+
if (needle.length === 0) {
|
|
207
|
+
const at = lines.length > 0 && lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
|
|
208
|
+
lines.splice(at, 0, ...replacement);
|
|
209
|
+
changed = true;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const verdict = findHunk(lines, needle);
|
|
213
|
+
if (verdict.ok === false)
|
|
214
|
+
return verdict;
|
|
215
|
+
lines.splice(verdict.index, needle.length, ...replacement);
|
|
216
|
+
changed = true;
|
|
217
|
+
}
|
|
218
|
+
if (!changed)
|
|
219
|
+
return { ok: false, output: "patch made no changes" };
|
|
220
|
+
return { ok: true, content: lines.join("\n") };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function findHunk(lines: string[], needle: string[]): { ok: true; index: number } | { ok: false; output: string } {
|
|
224
|
+
for (let level = 0; level < 3; level += 1) {
|
|
225
|
+
const matches = matchAll(lines, needle, level);
|
|
226
|
+
if (matches.length === 0)
|
|
227
|
+
continue;
|
|
228
|
+
if (matches.length > 1) {
|
|
229
|
+
return {
|
|
230
|
+
ok: false,
|
|
231
|
+
output: `ambiguous patch context (${matches.length} candidates at lines ${matches.map((index) => index + 1).join(", ")}); add surrounding context`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return { ok: true, index: matches[0] };
|
|
235
|
+
}
|
|
236
|
+
return { ok: false, output: `patch context not found: ${JSON.stringify(needle.slice(0, 4))}` };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function matchAll(lines: string[], needle: string[], level: number): number[] {
|
|
240
|
+
const found: number[] = [];
|
|
241
|
+
const wanted = needle.map((line) => normalizeLine(line, level));
|
|
242
|
+
for (let i = 0; i + needle.length <= lines.length; i += 1) {
|
|
243
|
+
let match = true;
|
|
244
|
+
for (let j = 0; j < needle.length; j += 1) {
|
|
245
|
+
if (normalizeLine(lines[i + j], level) !== wanted[j]) {
|
|
246
|
+
match = false;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (match)
|
|
251
|
+
found.push(i);
|
|
252
|
+
}
|
|
253
|
+
return found;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function normalizeLine(line: string, level: number): string {
|
|
257
|
+
if (level === 0)
|
|
258
|
+
return line;
|
|
259
|
+
if (level === 1)
|
|
260
|
+
return line.replace(/\s+$/, "");
|
|
261
|
+
return line.replace(/\s+/g, " ").trim();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function makeDiff(oldPath: string | null, newPath: string | null, oldContent: string, newContent: string, kind: "add" | "delete" | "update"): string {
|
|
265
|
+
const oldLines = splitDiffLines(oldContent);
|
|
266
|
+
const newLines = splitDiffLines(newContent);
|
|
267
|
+
let prefix = 0;
|
|
268
|
+
while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix])
|
|
269
|
+
prefix += 1;
|
|
270
|
+
let suffix = 0;
|
|
271
|
+
while (suffix < oldLines.length - prefix && suffix < newLines.length - prefix &&
|
|
272
|
+
oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix])
|
|
273
|
+
suffix += 1;
|
|
274
|
+
const oldMiddle = oldLines.slice(prefix, oldLines.length - suffix);
|
|
275
|
+
const newMiddle = newLines.slice(prefix, newLines.length - suffix);
|
|
276
|
+
const oldStart = prefix + 1;
|
|
277
|
+
const newStart = prefix + 1;
|
|
278
|
+
const header = kind === "add"
|
|
279
|
+
? `--- /dev/null\n+++ b/${newPath ?? ""}`
|
|
280
|
+
: kind === "delete"
|
|
281
|
+
? `--- a/${oldPath ?? ""}\n+++ /dev/null`
|
|
282
|
+
: `--- a/${oldPath ?? ""}\n+++ b/${newPath ?? oldPath ?? ""}`;
|
|
283
|
+
const body = [
|
|
284
|
+
`@@ -${oldStart},${oldMiddle.length} +${newStart},${newMiddle.length} @@`,
|
|
285
|
+
...oldMiddle.map((line) => "-" + line),
|
|
286
|
+
...newMiddle.map((line) => "+" + line),
|
|
287
|
+
];
|
|
288
|
+
return [header, ...body].join("\n");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function splitDiffLines(content: string): string[] {
|
|
292
|
+
if (content === "")
|
|
293
|
+
return [];
|
|
294
|
+
const lines = content.split(/\r?\n/);
|
|
295
|
+
if (lines.length > 0 && lines[lines.length - 1] === "")
|
|
296
|
+
lines.pop();
|
|
297
|
+
return lines;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function countChangedLines(oldContent: string, newContent: string): number {
|
|
301
|
+
const oldLines = oldContent.split(/\r?\n/);
|
|
302
|
+
const newLines = newContent.split(/\r?\n/);
|
|
303
|
+
let changed = 0;
|
|
304
|
+
let i = 0;
|
|
305
|
+
let j = 0;
|
|
306
|
+
while (i < oldLines.length && j < newLines.length) {
|
|
307
|
+
if (oldLines[i] === newLines[j]) {
|
|
308
|
+
i += 1;
|
|
309
|
+
j += 1;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
changed += 1;
|
|
313
|
+
i += 1;
|
|
314
|
+
j += 1;
|
|
315
|
+
}
|
|
316
|
+
return changed + Math.abs(oldLines.length - newLines.length);
|
|
317
|
+
}
|
|
@@ -10,12 +10,16 @@ const STATIC_MODELS = [
|
|
|
10
10
|
{ id: "claude-haiku-4.5", label: "Claude Haiku 4.5" },
|
|
11
11
|
];
|
|
12
12
|
const BASE = "https://api.anthropic.com/v1";
|
|
13
|
+
const THINKING_BUDGETS = { low: 1024, medium: 4096, high: 8192, max: 16000 };
|
|
13
14
|
export const anthropicProvider = {
|
|
14
15
|
id: "anthropic",
|
|
15
16
|
label: "Anthropic (Claude)",
|
|
16
17
|
authEnv: ["ANTHROPIC_API_KEY", "CLAUDE_API_KEY"],
|
|
17
18
|
baseUrl: BASE,
|
|
18
19
|
defaultModel: "claude-sonnet-5.5",
|
|
20
|
+
supportsReasoning() {
|
|
21
|
+
return true;
|
|
22
|
+
},
|
|
19
23
|
async listModels(apiKey) {
|
|
20
24
|
// Query the live model list; fall back to the built-in list offline.
|
|
21
25
|
if (apiKey) {
|
|
@@ -45,7 +49,7 @@ export const anthropicProvider = {
|
|
|
45
49
|
throw new Error("Missing ANTHROPIC_API_KEY. Bind one with /login anthropic <model> <key>, or set the env var.");
|
|
46
50
|
}
|
|
47
51
|
const messages = req.messages.map((m) => ({ role: m.role, content: m.content }));
|
|
48
|
-
const body = {
|
|
52
|
+
const body: any = {
|
|
49
53
|
model: req.model,
|
|
50
54
|
messages,
|
|
51
55
|
...(req.system ? { system: req.system } : {}),
|
|
@@ -53,6 +57,14 @@ export const anthropicProvider = {
|
|
|
53
57
|
max_tokens: req.maxOutputTokens ?? 4096,
|
|
54
58
|
...(req.temperature !== undefined ? { temperature: req.temperature } : {}),
|
|
55
59
|
};
|
|
60
|
+
if (req.reasoningEffort && req.reasoningEffort !== "auto") {
|
|
61
|
+
const budget = THINKING_BUDGETS[req.reasoningEffort] ?? THINKING_BUDGETS.medium;
|
|
62
|
+
body.thinking = { type: "enabled", budget_tokens: budget };
|
|
63
|
+
// max_tokens must exceed the thinking budget.
|
|
64
|
+
body.max_tokens = Math.max(body.max_tokens, budget + 1024);
|
|
65
|
+
// Extended thinking is incompatible with a custom temperature.
|
|
66
|
+
delete body.temperature;
|
|
67
|
+
}
|
|
56
68
|
const res = await fetch(BASE + "/messages", {
|
|
57
69
|
method: "POST",
|
|
58
70
|
headers: {
|
|
@@ -9,7 +9,8 @@ export const deepseekProvider = {
|
|
|
9
9
|
label: "DeepSeek",
|
|
10
10
|
authEnv: ["DEEPSEEK_API_KEY"],
|
|
11
11
|
baseUrl: BASE,
|
|
12
|
-
defaultModel: "deepseek-
|
|
12
|
+
defaultModel: "deepseek-flash",
|
|
13
13
|
listModels: (apiKey) => listModelsHttp({ baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
14
|
-
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
14
|
+
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC, reasoning: "openai" }),
|
|
15
|
+
supportsReasoning: () => true,
|
|
15
16
|
};
|
package/src/providers/groq.ts
CHANGED
|
@@ -16,5 +16,6 @@ export const groqProvider = {
|
|
|
16
16
|
baseUrl: BASE,
|
|
17
17
|
defaultModel: "llama-3.3-70b-versatile",
|
|
18
18
|
listModels: (apiKey) => listModelsHttp({ baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
19
|
-
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
19
|
+
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC, reasoning: "openai" }),
|
|
20
|
+
supportsReasoning: () => true,
|
|
20
21
|
};
|
package/src/providers/index.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { moonshotProvider } from "./moonshot.js";
|
|
|
11
11
|
import { zhipuProvider } from "./zhipu.js";
|
|
12
12
|
import { qwenProvider } from "./qwen.js";
|
|
13
13
|
import { customProvider } from "./custom.js";
|
|
14
|
-
|
|
14
|
+
import type { Provider } from "../types.js";
|
|
15
|
+
export const PROVIDERS: Provider[] = [
|
|
15
16
|
anthropicProvider,
|
|
16
17
|
openaiProvider,
|
|
17
18
|
googleProvider,
|
|
@@ -35,3 +36,33 @@ export function getProviderOrThrow(id) {
|
|
|
35
36
|
throw new Error("Unknown provider: " + id);
|
|
36
37
|
return p;
|
|
37
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Cheap connectivity check used by the model picker to label a list "offline".
|
|
41
|
+
* `listModels()` silently falls back to the static list, so without this probe
|
|
42
|
+
* the UI cannot tell a live catalog from a cached one.
|
|
43
|
+
*/
|
|
44
|
+
export async function probeProvider(provider, apiKey, timeoutMs = 4000) {
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
47
|
+
try {
|
|
48
|
+
const base = String(provider.baseUrl).replace(/[/]+$/, "");
|
|
49
|
+
const headers = { accept: "application/json" };
|
|
50
|
+
if (apiKey) {
|
|
51
|
+
if (provider.id === "anthropic") {
|
|
52
|
+
headers["x-api-key"] = apiKey;
|
|
53
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
headers["authorization"] = "Bearer " + apiKey;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const res = await fetch(base + "/models", { headers, signal: controller.signal });
|
|
60
|
+
return res.ok;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/providers/mistral.ts
CHANGED
|
@@ -15,5 +15,6 @@ export const mistralProvider = {
|
|
|
15
15
|
baseUrl: BASE,
|
|
16
16
|
defaultModel: "mistral-small-latest",
|
|
17
17
|
listModels: (apiKey) => listModelsHttp({ baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
18
|
-
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
18
|
+
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC, reasoning: "openai" }),
|
|
19
|
+
supportsReasoning: () => true,
|
|
19
20
|
};
|
|
@@ -18,5 +18,6 @@ export const moonshotProvider = {
|
|
|
18
18
|
},
|
|
19
19
|
defaultModel: "kimi-k3",
|
|
20
20
|
listModels: (apiKey) => listModelsHttp({ baseUrl: baseUrl(), apiKey, staticModels: STATIC }),
|
|
21
|
-
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: baseUrl(), apiKey, staticModels: STATIC }),
|
|
21
|
+
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: baseUrl(), apiKey, staticModels: STATIC, reasoning: "openai" }),
|
|
22
|
+
supportsReasoning: () => true,
|
|
22
23
|
};
|
package/src/providers/openai.ts
CHANGED
|
@@ -21,5 +21,6 @@ export const openaiProvider = {
|
|
|
21
21
|
baseUrl: BASE,
|
|
22
22
|
defaultModel: "gpt-6-luna",
|
|
23
23
|
listModels: (apiKey) => listModelsHttp({ baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
24
|
-
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
24
|
+
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC, reasoning: "openai" }),
|
|
25
|
+
supportsReasoning: (model) => /^(o\d|gpt-5|gpt-6)/i.test(model),
|
|
25
26
|
};
|
|
@@ -1,12 +1,69 @@
|
|
|
1
1
|
import { fetchJson, streamSSE } from "../lib/http.js";
|
|
2
|
+
import { finalizeToolCalls, mergeToolCallDelta, type ToolCallAccumulator } from "../lib/native-tools.js";
|
|
3
|
+
import { buildHeaders, buildRequestUrl, type RequestSpec } from "./spec.js";
|
|
4
|
+
|
|
5
|
+
export interface ModelInfo {
|
|
6
|
+
id: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
[key: string]: any;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface StreamOptions {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
chatPath?: string;
|
|
15
|
+
modelsPath?: string | null;
|
|
16
|
+
query?: Record<string, string>;
|
|
17
|
+
pathParams?: Record<string, string>;
|
|
18
|
+
authHeaderName?: string;
|
|
19
|
+
authPrefix?: string;
|
|
20
|
+
staticModels?: ModelInfo[];
|
|
21
|
+
extraHeaders?: Record<string, string>;
|
|
22
|
+
/** Legacy auth prefix, e.g. "Bearer". Prefer authPrefix for new callers. */
|
|
23
|
+
authHeader?: string;
|
|
24
|
+
reasoning?: "openai" | "openrouter" | "none" | string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function toRequestSpec(opts: StreamOptions): RequestSpec {
|
|
28
|
+
return {
|
|
29
|
+
baseUrl: trim(opts.baseUrl),
|
|
30
|
+
chatPath: opts.chatPath,
|
|
31
|
+
modelsPath: opts.modelsPath,
|
|
32
|
+
query: opts.query,
|
|
33
|
+
pathParams: opts.pathParams,
|
|
34
|
+
authHeaderName: opts.authHeaderName,
|
|
35
|
+
authPrefix: opts.authPrefix ?? (opts.authHeader !== undefined ? `${opts.authHeader} ` : undefined),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
2
38
|
/** Build a chat-completions body in the classic or the newer (compat) dialect. */
|
|
3
|
-
function buildBody(req, compat) {
|
|
39
|
+
function buildBody(req: any, compat: boolean, opts: StreamOptions, includeReasoning = true) {
|
|
4
40
|
const messages = [];
|
|
5
41
|
if (req.system)
|
|
6
42
|
messages.push({ role: "system", content: req.system });
|
|
7
|
-
for (const m of req.messages)
|
|
43
|
+
for (const m of req.messages) {
|
|
44
|
+
if (m.role === "tool") {
|
|
45
|
+
messages.push({ role: "tool", tool_call_id: m.toolCallId, content: m.content });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (m.role === "assistant" && m.toolCalls && m.toolCalls.length > 0) {
|
|
49
|
+
messages.push({
|
|
50
|
+
role: "assistant",
|
|
51
|
+
content: m.content || null,
|
|
52
|
+
tool_calls: m.toolCalls.map((call) => ({
|
|
53
|
+
id: call.id,
|
|
54
|
+
type: "function",
|
|
55
|
+
function: { name: call.name, arguments: call.arguments },
|
|
56
|
+
})),
|
|
57
|
+
});
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
8
60
|
messages.push({ role: m.role, content: m.content });
|
|
61
|
+
}
|
|
9
62
|
const body: any = { model: req.model, messages, stream: true };
|
|
63
|
+
if (req.tools && req.tools.length > 0) {
|
|
64
|
+
body.tools = req.tools;
|
|
65
|
+
body.tool_choice = "auto";
|
|
66
|
+
}
|
|
10
67
|
if (req.maxOutputTokens !== undefined) {
|
|
11
68
|
if (compat)
|
|
12
69
|
body.max_completion_tokens = req.maxOutputTokens;
|
|
@@ -15,38 +72,56 @@ function buildBody(req, compat) {
|
|
|
15
72
|
}
|
|
16
73
|
if (req.temperature !== undefined && !compat)
|
|
17
74
|
body.temperature = req.temperature;
|
|
75
|
+
if (includeReasoning && req.reasoningEffort && req.reasoningEffort !== "auto") {
|
|
76
|
+
const effort = req.reasoningEffort === "max" ? "high" : req.reasoningEffort;
|
|
77
|
+
if (opts.reasoning === "openai")
|
|
78
|
+
body.reasoning_effort = effort;
|
|
79
|
+
else if (opts.reasoning === "openrouter")
|
|
80
|
+
body.reasoning = { effort };
|
|
81
|
+
}
|
|
18
82
|
return body;
|
|
19
83
|
}
|
|
20
|
-
export async function* streamChatCompletion(req, opts) {
|
|
21
|
-
const
|
|
22
|
-
|
|
84
|
+
export async function* streamChatCompletion(req: any, opts: StreamOptions) {
|
|
85
|
+
const hasReasoning = Boolean(req.reasoningEffort && req.reasoningEffort !== "auto" && opts.reasoning && opts.reasoning !== "none");
|
|
86
|
+
const spec = toRequestSpec(opts);
|
|
87
|
+
const send = async (compat, includeReasoning = hasReasoning) => {
|
|
88
|
+
const headers: Record<string, string> = {
|
|
23
89
|
"content-type": "application/json",
|
|
24
90
|
accept: "text/event-stream",
|
|
25
91
|
...(opts.extraHeaders ?? {}),
|
|
26
92
|
};
|
|
27
93
|
if (opts.apiKey)
|
|
28
|
-
headers
|
|
29
|
-
|
|
94
|
+
Object.assign(headers, buildHeaders(spec, opts.apiKey));
|
|
95
|
+
if (req.clientRequestId)
|
|
96
|
+
headers["idempotency-key"] = req.clientRequestId;
|
|
97
|
+
return await fetch(buildRequestUrl(spec, opts.chatPath ?? "/chat/completions"), {
|
|
30
98
|
method: "POST",
|
|
31
99
|
headers,
|
|
32
|
-
body: JSON.stringify(buildBody(req, compat)),
|
|
100
|
+
body: JSON.stringify(buildBody(req, compat, opts, includeReasoning)),
|
|
33
101
|
signal: req.signal,
|
|
34
102
|
});
|
|
35
103
|
};
|
|
36
104
|
let res = await send(false);
|
|
37
105
|
if (!res.ok) {
|
|
38
|
-
|
|
106
|
+
let detail = await res.text().catch(() => "");
|
|
107
|
+
// Some models reject the thinking parameter even though the provider
|
|
108
|
+
// as a whole accepts it. Drop it once, then keep the request alive.
|
|
109
|
+
if (res.status === 400 && hasReasoning && /reasoning|thinking|effort|unsupported|unrecognized|unknown parameter/i.test(detail)) {
|
|
110
|
+
res = await send(false, false);
|
|
111
|
+
detail = res.ok ? detail : await res.text().catch(() => "");
|
|
112
|
+
}
|
|
39
113
|
// Newer OpenAI-style models reject max_tokens and temperature. Retry once
|
|
40
114
|
// in the compatibility dialect before surfacing the real error.
|
|
41
|
-
if (res.status === 400 && /max_tokens|max_completion_tokens|temperature|unsupported|unrecognized|unknown parameter/i.test(detail)) {
|
|
42
|
-
res = await send(true);
|
|
115
|
+
if (!res.ok && res.status === 400 && /max_tokens|max_completion_tokens|temperature|unsupported|unrecognized|unknown parameter/i.test(detail)) {
|
|
116
|
+
res = await send(true, false);
|
|
117
|
+
detail = res.ok ? detail : await res.text().catch(() => "");
|
|
43
118
|
}
|
|
44
119
|
if (!res.ok) {
|
|
45
|
-
|
|
46
|
-
throw new Error(res.status + " " + res.statusText + ": " + body.slice(0, 500));
|
|
120
|
+
throw new Error(res.status + " " + res.statusText + ": " + detail.slice(0, 500));
|
|
47
121
|
}
|
|
48
122
|
}
|
|
49
123
|
let usage;
|
|
124
|
+
const toolCalls: ToolCallAccumulator = new Map();
|
|
50
125
|
for await (const ev of streamSSE(res)) {
|
|
51
126
|
if (ev.data === "[DONE]")
|
|
52
127
|
break;
|
|
@@ -61,6 +136,14 @@ export async function* streamChatCompletion(req, opts) {
|
|
|
61
136
|
const choice = obj.choices?.[0];
|
|
62
137
|
const raw = choice?.delta?.content ?? choice?.text;
|
|
63
138
|
const reasoning = choice?.delta?.reasoning_content ?? choice?.delta?.reasoning;
|
|
139
|
+
if (Array.isArray(choice?.delta?.tool_calls)) {
|
|
140
|
+
for (const delta of choice.delta.tool_calls)
|
|
141
|
+
mergeToolCallDelta(toolCalls, delta);
|
|
142
|
+
}
|
|
143
|
+
if (Array.isArray(choice?.message?.tool_calls)) {
|
|
144
|
+
for (const delta of choice.message.tool_calls)
|
|
145
|
+
mergeToolCallDelta(toolCalls, delta);
|
|
146
|
+
}
|
|
64
147
|
if (obj.usage) {
|
|
65
148
|
usage = {
|
|
66
149
|
inputTokens: obj.usage.prompt_tokens,
|
|
@@ -78,17 +161,19 @@ export async function* streamChatCompletion(req, opts) {
|
|
|
78
161
|
else if (typeof reasoning === "string" && reasoning)
|
|
79
162
|
yield { delta: "", done: false, reasoning };
|
|
80
163
|
}
|
|
81
|
-
|
|
164
|
+
const calls = finalizeToolCalls(toolCalls);
|
|
165
|
+
yield calls.length > 0 ? { delta: "", done: true, usage, toolCalls: calls } : { delta: "", done: true, usage };
|
|
82
166
|
}
|
|
83
|
-
export async function listModelsHttp(opts) {
|
|
167
|
+
export async function listModelsHttp(opts: StreamOptions) {
|
|
84
168
|
const staticModels = opts.staticModels ?? [];
|
|
169
|
+
if (opts.modelsPath === null)
|
|
170
|
+
return staticModels;
|
|
85
171
|
if (!opts.apiKey && staticModels.length)
|
|
86
172
|
return staticModels;
|
|
87
173
|
try {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const json = await fetchJson(trim(opts.baseUrl) + "/models", { headers });
|
|
174
|
+
const spec = toRequestSpec(opts);
|
|
175
|
+
const headers = opts.apiKey ? buildHeaders(spec, opts.apiKey) : {};
|
|
176
|
+
const json = await fetchJson(buildRequestUrl(spec, opts.modelsPath ?? "/models"), { headers });
|
|
92
177
|
const ids = (json.data ?? [])
|
|
93
178
|
.map((m) => m.id)
|
|
94
179
|
.filter((id) => typeof id === "string" && id.length > 0);
|
|
@@ -22,5 +22,6 @@ export const openrouterProvider = {
|
|
|
22
22
|
baseUrl: BASE,
|
|
23
23
|
defaultModel: "anthropic/claude-sonnet-5.5",
|
|
24
24
|
listModels: (apiKey) => listModelsHttp({ baseUrl: BASE, apiKey, staticModels: STATIC, extraHeaders: EXTRA_HEADERS }),
|
|
25
|
-
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC, extraHeaders: EXTRA_HEADERS }),
|
|
25
|
+
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC, extraHeaders: EXTRA_HEADERS, reasoning: "openrouter" }),
|
|
26
|
+
supportsReasoning: () => true,
|
|
26
27
|
};
|
package/src/providers/qwen.ts
CHANGED
|
@@ -19,5 +19,6 @@ export const qwenProvider = {
|
|
|
19
19
|
},
|
|
20
20
|
defaultModel: "qwen3.8-max",
|
|
21
21
|
listModels: (apiKey) => listModelsHttp({ baseUrl: baseUrl(), apiKey, staticModels: STATIC }),
|
|
22
|
-
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: baseUrl(), apiKey, staticModels: STATIC }),
|
|
22
|
+
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: baseUrl(), apiKey, staticModels: STATIC, reasoning: "openai" }),
|
|
23
|
+
supportsReasoning: () => true,
|
|
23
24
|
};
|