@giovannijecha/jecode 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/controller.js +63 -14
- package/dist/credential-safety.js +19 -1
- package/dist/effort.js +25 -0
- package/dist/openai-account.js +10 -7
- package/dist/provider-commands.js +67 -14
- package/dist/providers/anthropic-stream.js +8 -2
- package/dist/providers/anthropic.js +21 -5
- package/dist/providers/ollama-stream.js +3 -0
- package/dist/providers/ollama.js +3 -0
- package/dist/providers/openai-codex.js +53 -10
- package/dist/providers/openai-stream.js +1 -1
- package/dist/providers/openai-wire.js +0 -5
- package/dist/providers/openai.js +34 -13
- package/dist/settings-command.js +53 -6
- package/dist/settings.js +2 -1
- package/dist/tools/args.js +4 -3
- package/dist/tools/fs.js +8 -3
- package/dist/tools/index.js +2 -2
- package/dist/tools/search.js +105 -20
- package/dist/tools/shell.js +30 -9
- package/dist/tui/app.js +17 -3
- package/dist/tui/components/messages.js +26 -5
- package/dist/tui/keys.js +33 -0
- package/dist/tui/screen.js +19 -5
- package/dist/tui/turn.js +4 -1
- package/dist/ui/width.js +41 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -255,9 +255,10 @@ untrusted data.
|
|
|
255
255
|
- Dangerous tools ask by default unless explicitly allowed for the session in
|
|
256
256
|
**/permissions** or the process started with **--auto-approve**.
|
|
257
257
|
- Credential fields are masked and excluded from transcripts. Approved shell
|
|
258
|
-
commands receive no
|
|
259
|
-
|
|
260
|
-
|
|
258
|
+
commands receive no secret-bearing environment variables; `SSH_AUTH_SOCK`
|
|
259
|
+
is preserved so Git and SSH can use the user's agent, which means an approved
|
|
260
|
+
command can request that agent to authenticate. Recognized credential values
|
|
261
|
+
are redacted before tool output reaches the model, screen, history, or export.
|
|
261
262
|
- ChatGPT OAuth uses PKCE and an exact loopback callback or the OpenAI device
|
|
262
263
|
flow. Refresh-token rotation is serialized across Jecode processes; OAuth
|
|
263
264
|
tokens are withheld and redacted like API keys.
|
package/dist/controller.js
CHANGED
|
@@ -14,6 +14,7 @@ export const MAX_TOOL_CALLS_PER_STEP = 32;
|
|
|
14
14
|
export async function runTurn(history, options, events, signal) {
|
|
15
15
|
const specs = toolSpecs(options.tools);
|
|
16
16
|
for (let step = 0; step < options.maxSteps; step++) {
|
|
17
|
+
throwIfAborted(signal);
|
|
17
18
|
events.onStep?.(step + 1, options.maxSteps);
|
|
18
19
|
// The message is displayed as it streams; what comes back here is the
|
|
19
20
|
// assembled version, which exists to be appended to the history.
|
|
@@ -28,6 +29,7 @@ export async function runTurn(history, options, events, signal) {
|
|
|
28
29
|
onStream: (event) => events.onStream(event),
|
|
29
30
|
onStatus: (status) => events.onStatus?.(status),
|
|
30
31
|
});
|
|
32
|
+
throwIfAborted(signal);
|
|
31
33
|
const calls = assistant.content.filter(isToolCall);
|
|
32
34
|
if (assistant.content.length === 0) {
|
|
33
35
|
throw new Error(`${options.provider.id} completed without an answer or tool call`);
|
|
@@ -36,22 +38,58 @@ export async function runTurn(history, options, events, signal) {
|
|
|
36
38
|
throw new Error(`provider returned ${calls.length} tool calls in one step (maximum ${MAX_TOOL_CALLS_PER_STEP})`);
|
|
37
39
|
}
|
|
38
40
|
history.push(assistant);
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
if (calls.length === 0) {
|
|
42
|
+
if (assistant.usage !== undefined)
|
|
43
|
+
events.onUsage?.(assistant.usage);
|
|
42
44
|
return; // the model is done — hand back to the user
|
|
45
|
+
}
|
|
43
46
|
// Calls run one after another because approval prompts serialise anyway,
|
|
44
47
|
// but every result from this step goes back in a SINGLE message. Splitting
|
|
45
48
|
// them teaches the model to stop batching its calls.
|
|
46
49
|
const results = [];
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
const announced = new Set();
|
|
51
|
+
try {
|
|
52
|
+
if (assistant.usage !== undefined)
|
|
53
|
+
events.onUsage?.(assistant.usage);
|
|
54
|
+
for (let index = 0; index < calls.length; index++) {
|
|
55
|
+
throwIfAborted(signal);
|
|
56
|
+
const call = calls[index];
|
|
57
|
+
events.onToolProgress?.(index + 1, calls.length);
|
|
58
|
+
const preview = await look(call, options, signal);
|
|
59
|
+
throwIfAborted(signal);
|
|
60
|
+
announced.add(call.id);
|
|
61
|
+
events.onToolCall(call, preview);
|
|
62
|
+
const { result, summary } = await settle(call, options, events, signal, preview);
|
|
63
|
+
results.push(result);
|
|
64
|
+
events.onToolResult(call, result, summary);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const interrupted = signal?.aborted === true;
|
|
69
|
+
const repairs = [];
|
|
70
|
+
for (const call of calls.slice(results.length)) {
|
|
71
|
+
const run = interrupted
|
|
72
|
+
? refuse(call, "interrupted before completion", "interrupted")
|
|
73
|
+
: refuse(call, "tool processing stopped before completion", "failed");
|
|
74
|
+
repairs.push({ call, run });
|
|
75
|
+
results.push(run.result);
|
|
76
|
+
}
|
|
77
|
+
history.push({ role: "user", content: results });
|
|
78
|
+
// History repair is the invariant. UI recovery is best-effort and must
|
|
79
|
+
// never replace the original exception or leave the conversation open.
|
|
80
|
+
for (const { call, run } of repairs) {
|
|
81
|
+
if (!announced.has(call.id))
|
|
82
|
+
continue;
|
|
83
|
+
try {
|
|
84
|
+
events.onToolResult(call, run.result, run.summary);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// The surface is already failing; the next turn can still proceed.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (interrupted)
|
|
91
|
+
throw abortReason(signal);
|
|
92
|
+
throw error;
|
|
55
93
|
}
|
|
56
94
|
history.push({ role: "user", content: results });
|
|
57
95
|
}
|
|
@@ -62,9 +100,13 @@ async function settle(call, options, events, signal, preview) {
|
|
|
62
100
|
if (tool === undefined) {
|
|
63
101
|
return refuse(call, `no such tool: ${call.name}`, "unknown tool");
|
|
64
102
|
}
|
|
65
|
-
|
|
103
|
+
throwIfAborted(signal);
|
|
104
|
+
const approved = !tool.dangerous || await events.approve(call);
|
|
105
|
+
throwIfAborted(signal);
|
|
106
|
+
if (!approved) {
|
|
66
107
|
return refuse(call, "the user declined this call — ask them how to proceed", "declined");
|
|
67
108
|
}
|
|
109
|
+
throwIfAborted(signal);
|
|
68
110
|
return runTool(tool, call, {
|
|
69
111
|
...options.toolContext,
|
|
70
112
|
signal,
|
|
@@ -85,14 +127,21 @@ function refuse(call, reason, summary) {
|
|
|
85
127
|
* file, a match that is not there — is simply no preview. Nothing about the
|
|
86
128
|
* turn depends on it, because it exists for the user, not for the model.
|
|
87
129
|
*/
|
|
88
|
-
async function look(call, options) {
|
|
130
|
+
async function look(call, options, signal) {
|
|
89
131
|
const tool = findTool(options.tools, call.name);
|
|
90
132
|
if (tool?.preview === undefined)
|
|
91
133
|
return undefined;
|
|
92
134
|
try {
|
|
93
|
-
return await tool.preview(call.input, options.toolContext);
|
|
135
|
+
return await tool.preview(call.input, { ...options.toolContext, signal });
|
|
94
136
|
}
|
|
95
137
|
catch {
|
|
96
138
|
return undefined;
|
|
97
139
|
}
|
|
98
140
|
}
|
|
141
|
+
function throwIfAborted(signal) {
|
|
142
|
+
if (signal?.aborted === true)
|
|
143
|
+
throw abortReason(signal);
|
|
144
|
+
}
|
|
145
|
+
function abortReason(signal) {
|
|
146
|
+
return signal.reason instanceof Error ? signal.reason : new Error("interrupted");
|
|
147
|
+
}
|
|
@@ -2,8 +2,20 @@
|
|
|
2
2
|
import { credentialValues } from "./credentials.js";
|
|
3
3
|
import { accountValues } from "./accounts.js";
|
|
4
4
|
const REDACTED = "[credential redacted]";
|
|
5
|
+
const MIN_HEURISTIC_SECRET_CHARS = 8;
|
|
6
|
+
const EXPLICIT_CREDENTIAL_ENVIRONMENT_NAMES = new Set([
|
|
7
|
+
"ANTHROPIC_API_KEY",
|
|
8
|
+
"OLLAMA_API_KEY",
|
|
9
|
+
"OPENAI_API_KEY",
|
|
10
|
+
]);
|
|
5
11
|
const SENSITIVE_ENVIRONMENT_NAME = /(?:^|_)(?:API_?KEY|ACCESS_?KEY|PRIVATE_?KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|PASS|PWD|CREDENTIALS?|AUTH|JWT|COOKIE|PAT)(?:_|$)/i;
|
|
6
12
|
const COMPACT_SENSITIVE_ENVIRONMENT_NAME = /^(?:PGPASSWORD)$/i;
|
|
13
|
+
const SAFE_ENVIRONMENT_NAMES = new Set([
|
|
14
|
+
"SSH_AUTH_SOCK",
|
|
15
|
+
"PWD",
|
|
16
|
+
"OLDPWD",
|
|
17
|
+
"PASSWORD_STORE_DIR",
|
|
18
|
+
]);
|
|
7
19
|
/** Preserve ordinary tool configuration while withholding credential-like values. */
|
|
8
20
|
export function shellEnvironment(source = process.env) {
|
|
9
21
|
const environment = {};
|
|
@@ -57,14 +69,20 @@ function sensitiveEnvironmentName(name) {
|
|
|
57
69
|
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
58
70
|
.replace(/[^a-z0-9]+/gi, "_")
|
|
59
71
|
.replace(/^_+|_+$/g, "");
|
|
72
|
+
if (SAFE_ENVIRONMENT_NAMES.has(normalized.toUpperCase()))
|
|
73
|
+
return false;
|
|
60
74
|
return (SENSITIVE_ENVIRONMENT_NAME.test(normalized) ||
|
|
61
75
|
COMPACT_SENSITIVE_ENVIRONMENT_NAME.test(normalized));
|
|
62
76
|
}
|
|
63
77
|
function secrets(source) {
|
|
64
78
|
const values = new Set([...credentialValues(), ...accountValues()]);
|
|
65
79
|
for (const [name, value] of Object.entries(source)) {
|
|
66
|
-
if (value
|
|
80
|
+
if (value === undefined || value === "")
|
|
81
|
+
continue;
|
|
82
|
+
const explicit = EXPLICIT_CREDENTIAL_ENVIRONMENT_NAMES.has(name.toUpperCase());
|
|
83
|
+
if (explicit || (value.length >= MIN_HEURISTIC_SECRET_CHARS && sensitiveEnvironment(name, value))) {
|
|
67
84
|
values.add(value);
|
|
85
|
+
}
|
|
68
86
|
}
|
|
69
87
|
return [...values].filter((value) => value !== "").sort((left, right) => right.length - left.length);
|
|
70
88
|
}
|
package/dist/effort.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// One ordered vocabulary for reasoning depth. Providers expose the subset a
|
|
2
|
+
// selected model accepts; this module only validates and reconciles that data.
|
|
3
|
+
export const EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
4
|
+
export function isEffort(value) {
|
|
5
|
+
return typeof value === "string" && EFFORTS.includes(value);
|
|
6
|
+
}
|
|
7
|
+
export function requireSupportedEffort(model, requested, supported) {
|
|
8
|
+
if (supported.includes(requested))
|
|
9
|
+
return requested;
|
|
10
|
+
const choices = supported.length === 0 ? "none" : supported.join(", ");
|
|
11
|
+
throw new Error(`${model} does not support effort "${requested}" (available: ${choices})`);
|
|
12
|
+
}
|
|
13
|
+
/** Keep the requested depth when possible, otherwise choose the nearest lower level. */
|
|
14
|
+
export function compatibleEffort(requested, supported) {
|
|
15
|
+
if (supported.length === 0)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (supported.includes(requested))
|
|
18
|
+
return requested;
|
|
19
|
+
const requestedIndex = EFFORTS.indexOf(requested);
|
|
20
|
+
const candidates = EFFORTS.filter((value) => supported.includes(value));
|
|
21
|
+
if (requestedIndex < 0)
|
|
22
|
+
return candidates[0];
|
|
23
|
+
return [...candidates].reverse().find((value) => EFFORTS.indexOf(value) < requestedIndex)
|
|
24
|
+
?? candidates[0];
|
|
25
|
+
}
|
package/dist/openai-account.js
CHANGED
|
@@ -43,6 +43,8 @@ export async function openAIAuthorization(forceToken, signal, onStatus) {
|
|
|
43
43
|
async function refreshAccount(forceToken, signal, onStatus) {
|
|
44
44
|
if (refreshing !== undefined)
|
|
45
45
|
return abortable(refreshing, signal);
|
|
46
|
+
if (signal?.aborted === true)
|
|
47
|
+
throw abortReason(signal);
|
|
46
48
|
onStatus?.("Refreshing ChatGPT sign-in");
|
|
47
49
|
const task = updateOpenAICodexAccount(async (current) => {
|
|
48
50
|
if (current === undefined)
|
|
@@ -51,20 +53,21 @@ async function refreshAccount(forceToken, signal, onStatus) {
|
|
|
51
53
|
return current;
|
|
52
54
|
if (forceToken === undefined && !expiresSoon(current))
|
|
53
55
|
return current;
|
|
54
|
-
return refreshOpenAITokens(current
|
|
55
|
-
}
|
|
56
|
+
return refreshOpenAITokens(current);
|
|
57
|
+
}).then((account) => {
|
|
56
58
|
if (account === undefined)
|
|
57
59
|
throw new Error("ChatGPT account is not connected");
|
|
58
60
|
return account;
|
|
59
61
|
});
|
|
60
62
|
refreshing = task;
|
|
61
|
-
|
|
62
|
-
return await abortable(task, signal);
|
|
63
|
-
}
|
|
64
|
-
finally {
|
|
63
|
+
void task.then(() => {
|
|
65
64
|
if (refreshing === task)
|
|
66
65
|
refreshing = undefined;
|
|
67
|
-
}
|
|
66
|
+
}, () => {
|
|
67
|
+
if (refreshing === task)
|
|
68
|
+
refreshing = undefined;
|
|
69
|
+
});
|
|
70
|
+
return abortable(task, signal);
|
|
68
71
|
}
|
|
69
72
|
function expiresSoon(account) {
|
|
70
73
|
return account.expiresAt - Date.now() <= REFRESH_EARLY_MS;
|
|
@@ -5,6 +5,7 @@ import { readSettings } from "./settings.js";
|
|
|
5
5
|
import { authenticationNeed, ensureProviderAuthentication, } from "./credential-commands.js";
|
|
6
6
|
import { providerFailure } from "./provider-errors.js";
|
|
7
7
|
import { providerLabel } from "./provider-label.js";
|
|
8
|
+
import { compatibleEffort } from "./effort.js";
|
|
8
9
|
/**
|
|
9
10
|
* The provider menu.
|
|
10
11
|
*
|
|
@@ -61,6 +62,7 @@ export async function providersCommand(session, host, behavior = {}) {
|
|
|
61
62
|
model: session.model,
|
|
62
63
|
providerId: session.config.providerId,
|
|
63
64
|
configModel: session.config.model,
|
|
65
|
+
effort: session.config.effort,
|
|
64
66
|
};
|
|
65
67
|
session.provider = chosen;
|
|
66
68
|
// The model belonged to the old provider. Carrying it across would send
|
|
@@ -68,23 +70,31 @@ export async function providersCommand(session, host, behavior = {}) {
|
|
|
68
70
|
session.model = readSettings().models?.[chosen.id] ?? chosen.defaultModel;
|
|
69
71
|
session.config.providerId = chosen.id;
|
|
70
72
|
session.config.model = session.model;
|
|
71
|
-
if (session.model === ""
|
|
72
|
-
session
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
if (session.model === "") {
|
|
74
|
+
if (!(await modelsCommand(session, host, { announce: false, save: false }))) {
|
|
75
|
+
restoreProvider(session, before);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const alignment = await alignEffort(session, host);
|
|
81
|
+
if (!alignment.ok) {
|
|
82
|
+
restoreProvider(session, before);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
77
85
|
}
|
|
78
86
|
if (behavior.save !== false) {
|
|
79
87
|
const saved = readSettings();
|
|
80
88
|
const models = { ...saved.models };
|
|
81
89
|
if (session.model !== "")
|
|
82
90
|
models[chosen.id] = session.model;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
session.config.
|
|
87
|
-
|
|
91
|
+
const patch = {
|
|
92
|
+
provider: chosen.id,
|
|
93
|
+
models,
|
|
94
|
+
...(session.config.effort === before.effort ? {} : { effort: session.config.effort }),
|
|
95
|
+
};
|
|
96
|
+
if (!(await saveDefaults(host, patch))) {
|
|
97
|
+
restoreProvider(session, before);
|
|
88
98
|
return false;
|
|
89
99
|
}
|
|
90
100
|
}
|
|
@@ -152,15 +162,31 @@ export async function modelsCommand(session, host, behavior = {}) {
|
|
|
152
162
|
const chosen = ids[index];
|
|
153
163
|
if (chosen === undefined)
|
|
154
164
|
return false;
|
|
155
|
-
const before = {
|
|
165
|
+
const before = {
|
|
166
|
+
model: session.model,
|
|
167
|
+
configModel: session.config.model,
|
|
168
|
+
effort: session.config.effort,
|
|
169
|
+
};
|
|
156
170
|
session.model = chosen;
|
|
157
171
|
session.config.model = chosen;
|
|
172
|
+
const alignment = await alignEffort(session, host);
|
|
173
|
+
if (!alignment.ok) {
|
|
174
|
+
session.model = before.model;
|
|
175
|
+
session.config.model = before.configModel;
|
|
176
|
+
session.config.effort = before.effort;
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
158
179
|
if (behavior.save !== false) {
|
|
159
180
|
const saved = readSettings();
|
|
160
181
|
const models = { ...saved.models, [provider.id]: chosen };
|
|
161
|
-
|
|
182
|
+
const patch = {
|
|
183
|
+
models,
|
|
184
|
+
...(session.config.effort === before.effort ? {} : { effort: session.config.effort }),
|
|
185
|
+
};
|
|
186
|
+
if (!(await saveDefaults(host, patch))) {
|
|
162
187
|
session.model = before.model;
|
|
163
188
|
session.config.model = before.configModel;
|
|
189
|
+
session.config.effort = before.effort;
|
|
164
190
|
return false;
|
|
165
191
|
}
|
|
166
192
|
}
|
|
@@ -169,7 +195,34 @@ export async function modelsCommand(session, host, behavior = {}) {
|
|
|
169
195
|
}
|
|
170
196
|
return true;
|
|
171
197
|
}
|
|
172
|
-
|
|
198
|
+
function restoreProvider(session, before) {
|
|
199
|
+
session.provider = before.provider;
|
|
200
|
+
session.model = before.model;
|
|
201
|
+
session.config.providerId = before.providerId;
|
|
202
|
+
session.config.model = before.configModel;
|
|
203
|
+
session.config.effort = before.effort;
|
|
204
|
+
}
|
|
205
|
+
async function alignEffort(session, host) {
|
|
206
|
+
if (session.provider.efforts === undefined)
|
|
207
|
+
return { ok: true };
|
|
208
|
+
let supported;
|
|
209
|
+
try {
|
|
210
|
+
supported = await session.provider.efforts(session.model, host.signal, (status) => host.status?.(status));
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
host.emit({
|
|
214
|
+
kind: "notice",
|
|
215
|
+
text: providerFailure(session.provider, error, true),
|
|
216
|
+
tone: "error",
|
|
217
|
+
});
|
|
218
|
+
return { ok: false };
|
|
219
|
+
}
|
|
220
|
+
const adjusted = compatibleEffort(session.config.effort, supported);
|
|
221
|
+
if (adjusted === undefined || adjusted === session.config.effort)
|
|
222
|
+
return { ok: true };
|
|
223
|
+
session.config.effort = adjusted;
|
|
224
|
+
return { ok: true, adjusted };
|
|
225
|
+
}
|
|
173
226
|
function chooser(host) {
|
|
174
227
|
if (host.choose === undefined) {
|
|
175
228
|
host.emit({ kind: "notice", text: "that command needs the screen", tone: "warn" });
|
|
@@ -13,7 +13,8 @@ export async function assembleAnthropic(events, onStream) {
|
|
|
13
13
|
let stopReason;
|
|
14
14
|
let stopDetails;
|
|
15
15
|
let usage;
|
|
16
|
-
|
|
16
|
+
let complete = false;
|
|
17
|
+
stream: for await (const raw of events) {
|
|
17
18
|
const event = raw;
|
|
18
19
|
switch (event.type) {
|
|
19
20
|
case "message_start":
|
|
@@ -51,10 +52,15 @@ export async function assembleAnthropic(events, onStream) {
|
|
|
51
52
|
case "error": {
|
|
52
53
|
throw new Error(`anthropic stream error: ${event.error?.message ?? "unspecified"}`);
|
|
53
54
|
}
|
|
55
|
+
case "message_stop":
|
|
56
|
+
complete = true;
|
|
57
|
+
break stream;
|
|
54
58
|
default:
|
|
55
|
-
break; //
|
|
59
|
+
break; // ping and unknown forward-compatible events
|
|
56
60
|
}
|
|
57
61
|
}
|
|
62
|
+
if (!complete)
|
|
63
|
+
throw new Error("anthropic stream ended before message_stop");
|
|
58
64
|
const content = [...blocks.entries()]
|
|
59
65
|
.sort(([a], [b]) => a - b)
|
|
60
66
|
.map(([, block]) => block);
|
|
@@ -9,19 +9,27 @@
|
|
|
9
9
|
import { postSse } from "./http.js";
|
|
10
10
|
import { listModels } from "./catalog.js";
|
|
11
11
|
import { keyFor } from "../credentials.js";
|
|
12
|
+
import { EFFORTS, requireSupportedEffort } from "../effort.js";
|
|
12
13
|
import { assembleAnthropic } from "./anthropic-stream.js";
|
|
13
14
|
import { fromWireResponse, stopNotice, toWireMessage, toWireTool } from "./anthropic-wire.js";
|
|
14
15
|
const ENDPOINT = "https://api.anthropic.com/v1/messages";
|
|
15
16
|
const MODELS = "https://api.anthropic.com/v1/models?limit=100";
|
|
16
17
|
const API_VERSION = "2023-06-01";
|
|
17
18
|
const KEY = "ANTHROPIC_API_KEY";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const ADAPTIVE = /^claude-(fable-5|opus-(5|4-[678])|sonnet-(5|4-6))/;
|
|
19
|
+
const ADAPTIVE = /^claude-(?:fable-5|mythos-(?:5|preview)|opus-(?:5|4-[678])|sonnet-(?:5|4-6))(?:-|$)/;
|
|
20
|
+
const MAX_WITHOUT_XHIGH = ["low", "medium", "high", "max"];
|
|
21
|
+
const ANTHROPIC_45_EFFORTS = ["low", "medium", "high"];
|
|
22
22
|
export function supportsAdaptiveThinking(model) {
|
|
23
23
|
return ADAPTIVE.test(model);
|
|
24
24
|
}
|
|
25
|
+
export function anthropicEfforts(model) {
|
|
26
|
+
if (/^claude-(?:(?:opus|sonnet)-4-6|mythos-preview)(?:-|$)/.test(model)) {
|
|
27
|
+
return MAX_WITHOUT_XHIGH;
|
|
28
|
+
}
|
|
29
|
+
if (/^claude-opus-4-5(?:-|$)/.test(model))
|
|
30
|
+
return ANTHROPIC_45_EFFORTS;
|
|
31
|
+
return supportsAdaptiveThinking(model) ? EFFORTS : [];
|
|
32
|
+
}
|
|
25
33
|
export const anthropic = {
|
|
26
34
|
id: "anthropic",
|
|
27
35
|
// Sonnet is the default because it is the one that can be left running.
|
|
@@ -36,6 +44,9 @@ export const anthropic = {
|
|
|
36
44
|
models(signal, onStatus) {
|
|
37
45
|
return listModels(MODELS, headers(requireKey()), signal, onStatus);
|
|
38
46
|
},
|
|
47
|
+
async efforts(model) {
|
|
48
|
+
return anthropicEfforts(model);
|
|
49
|
+
},
|
|
39
50
|
location: () => "cloud",
|
|
40
51
|
async send(req) {
|
|
41
52
|
const key = requireKey();
|
|
@@ -49,7 +60,12 @@ export const anthropic = {
|
|
|
49
60
|
};
|
|
50
61
|
if (supportsAdaptiveThinking(req.model)) {
|
|
51
62
|
body["thinking"] = { type: "adaptive", display: "summarized" };
|
|
52
|
-
|
|
63
|
+
}
|
|
64
|
+
const efforts = anthropicEfforts(req.model);
|
|
65
|
+
if (efforts.length > 0) {
|
|
66
|
+
body["output_config"] = {
|
|
67
|
+
effort: requireSupportedEffort(req.model, req.effort, efforts),
|
|
68
|
+
};
|
|
53
69
|
}
|
|
54
70
|
const events = await postSse(ENDPOINT, headers(key), body, req.signal, req.onStatus);
|
|
55
71
|
const data = await assembleAnthropic(events, req.onStream);
|
|
@@ -51,6 +51,9 @@ export async function assembleOllama(events, onStream) {
|
|
|
51
51
|
calls.set(index, call);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
if (finishReason === undefined) {
|
|
55
|
+
throw new Error("ollama stream ended before a finish reason");
|
|
56
|
+
}
|
|
54
57
|
const toolCalls = [...calls.entries()]
|
|
55
58
|
.sort(([a], [b]) => a - b)
|
|
56
59
|
// Not every server sends an id, and the loop needs one to pair the result
|
package/dist/providers/ollama.js
CHANGED
|
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { openAICodexAccount } from "../accounts.js";
|
|
4
4
|
import { openAIAuthorization } from "../openai-account.js";
|
|
5
5
|
import { applicationVersion } from "../version.js";
|
|
6
|
+
import { EFFORTS, isEffort, requireSupportedEffort } from "../effort.js";
|
|
6
7
|
import { getJson, postSse } from "./http.js";
|
|
7
8
|
import { assembleOpenAI } from "./openai-stream.js";
|
|
8
9
|
import { fromWireResponse, stopNotice, toWireItems, toWireTool, } from "./openai-wire.js";
|
|
@@ -16,6 +17,8 @@ const SESSION_ID = randomUUID();
|
|
|
16
17
|
const MAX_CATALOG_ITEMS = 4_000;
|
|
17
18
|
const MAX_MODELS = 1_000;
|
|
18
19
|
const MAX_MODEL_CHARS = 256;
|
|
20
|
+
const XHIGH_EFFORTS = ["low", "medium", "high", "xhigh"];
|
|
21
|
+
let effortByModel = new Map();
|
|
19
22
|
export const openaiCodex = {
|
|
20
23
|
id: ID,
|
|
21
24
|
defaultModel: "",
|
|
@@ -25,12 +28,21 @@ export const openaiCodex = {
|
|
|
25
28
|
},
|
|
26
29
|
location: () => "cloud",
|
|
27
30
|
async models(signal, onStatus) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
const catalog = await loadCatalog(signal, onStatus);
|
|
32
|
+
effortByModel = catalog.efforts;
|
|
33
|
+
return catalog.ids;
|
|
34
|
+
},
|
|
35
|
+
async efforts(model, signal, onStatus) {
|
|
36
|
+
const cached = effortByModel.get(model);
|
|
37
|
+
if (cached !== undefined)
|
|
38
|
+
return cached;
|
|
39
|
+
const catalog = await loadCatalog(signal, onStatus);
|
|
40
|
+
effortByModel = catalog.efforts;
|
|
41
|
+
return effortByModel.get(model) ?? fallbackEfforts(model);
|
|
32
42
|
},
|
|
33
43
|
async send(req) {
|
|
44
|
+
const efforts = effortByModel.get(req.model) ?? fallbackEfforts(req.model);
|
|
45
|
+
const effort = requireSupportedEffort(req.model, req.effort, efforts);
|
|
34
46
|
return withAuthorization(async (authorization) => {
|
|
35
47
|
const events = await postSse(`${BASE}/responses`, {
|
|
36
48
|
...headers(authorization, randomUUID()),
|
|
@@ -44,7 +56,7 @@ export const openaiCodex = {
|
|
|
44
56
|
tools: req.tools.map(toWireTool),
|
|
45
57
|
tool_choice: "auto",
|
|
46
58
|
parallel_tool_calls: true,
|
|
47
|
-
reasoning: { effort
|
|
59
|
+
reasoning: { effort, summary: "auto" },
|
|
48
60
|
text: { verbosity: "low" },
|
|
49
61
|
include: ["reasoning.encrypted_content"],
|
|
50
62
|
prompt_cache_key: SESSION_ID,
|
|
@@ -57,6 +69,12 @@ export const openaiCodex = {
|
|
|
57
69
|
}, req.signal, req.onStatus);
|
|
58
70
|
},
|
|
59
71
|
};
|
|
72
|
+
async function loadCatalog(signal, onStatus) {
|
|
73
|
+
return withAuthorization(async (authorization) => {
|
|
74
|
+
const body = await getJson(`${BASE}/models?client_version=${CATALOG_COMPATIBILITY_VERSION}`, headers(authorization, randomUUID()), signal, onStatus);
|
|
75
|
+
return modelCatalog(body);
|
|
76
|
+
}, signal, onStatus);
|
|
77
|
+
}
|
|
60
78
|
async function withAuthorization(operation, signal, onStatus) {
|
|
61
79
|
let authorization = await openAIAuthorization(undefined, signal, onStatus);
|
|
62
80
|
try {
|
|
@@ -80,12 +98,12 @@ function headers(authorization, requestId) {
|
|
|
80
98
|
"x-client-request-id": requestId,
|
|
81
99
|
};
|
|
82
100
|
}
|
|
83
|
-
function
|
|
101
|
+
function modelCatalog(value) {
|
|
84
102
|
const source = record(value) && Array.isArray(value["models"]) ? value["models"] : undefined;
|
|
85
103
|
if (source === undefined)
|
|
86
104
|
throw new Error("OpenAI Codex did not return a model list");
|
|
87
105
|
const seen = new Set();
|
|
88
|
-
|
|
106
|
+
const models = source
|
|
89
107
|
.slice(0, MAX_CATALOG_ITEMS)
|
|
90
108
|
.flatMap((entry) => {
|
|
91
109
|
if (!record(entry))
|
|
@@ -98,11 +116,36 @@ function modelIds(value) {
|
|
|
98
116
|
seen.has(id))
|
|
99
117
|
return [];
|
|
100
118
|
seen.add(id);
|
|
101
|
-
return [{
|
|
119
|
+
return [{
|
|
120
|
+
id,
|
|
121
|
+
priority: typeof entry["priority"] === "number" ? entry["priority"] : 0,
|
|
122
|
+
efforts: reasoningLevels(entry, id),
|
|
123
|
+
}];
|
|
102
124
|
})
|
|
103
125
|
.sort((left, right) => left.priority - right.priority)
|
|
104
|
-
.slice(0, MAX_MODELS)
|
|
105
|
-
|
|
126
|
+
.slice(0, MAX_MODELS);
|
|
127
|
+
return {
|
|
128
|
+
ids: models.map((entry) => entry.id),
|
|
129
|
+
efforts: new Map(models.map((entry) => [entry.id, entry.efforts])),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function reasoningLevels(entry, model) {
|
|
133
|
+
const source = entry["supported_reasoning_levels"];
|
|
134
|
+
if (!Array.isArray(source))
|
|
135
|
+
return fallbackEfforts(model);
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
const efforts = source.flatMap((level) => {
|
|
138
|
+
if (!record(level) || !isEffort(level["effort"]) || seen.has(level["effort"]))
|
|
139
|
+
return [];
|
|
140
|
+
seen.add(level["effort"]);
|
|
141
|
+
return [level["effort"]];
|
|
142
|
+
});
|
|
143
|
+
return efforts.length === 0 ? fallbackEfforts(model) : efforts;
|
|
144
|
+
}
|
|
145
|
+
function fallbackEfforts(model) {
|
|
146
|
+
if (/^gpt-5\.6-(?:sol|terra|luna)(?:-|$)/.test(model))
|
|
147
|
+
return EFFORTS;
|
|
148
|
+
return XHIGH_EFFORTS;
|
|
106
149
|
}
|
|
107
150
|
function statusOf(error) {
|
|
108
151
|
return typeof error === "object" && error !== null && "status" in error
|
|
@@ -42,7 +42,7 @@ export async function assembleOpenAI(events, onStream) {
|
|
|
42
42
|
break;
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
throw new Error("openai stream ended before a terminal response event");
|
|
46
46
|
}
|
|
47
47
|
function reconcileOutput(completed, items) {
|
|
48
48
|
if (completed === undefined)
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
// Translation between the normalized vocabulary and the OpenAI Responses wire
|
|
2
2
|
// shape: a flat `input` list where tool calls and their outputs are top-level
|
|
3
3
|
// items keyed by `call_id`, rather than blocks nested inside a message.
|
|
4
|
-
// The Responses API has no `xhigh`; collapse it onto the nearest level rather
|
|
5
|
-
// than passing through a value it will reject.
|
|
6
|
-
export function normalizeEffort(effort) {
|
|
7
|
-
return effort === "xhigh" || effort === "max" ? "high" : effort;
|
|
8
|
-
}
|
|
9
4
|
export function toWireTool(tool) {
|
|
10
5
|
return {
|
|
11
6
|
type: "function",
|