@gullabs/claude-cli 0.7.0 → 0.16.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/NOTICE +6 -0
- package/README.md +73 -7
- package/dist/index.cjs +227 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -14
- package/dist/index.d.ts +34 -14
- package/dist/index.js +228 -33
- package/dist/index.js.map +1 -1
- package/package.json +18 -8
package/NOTICE
ADDED
package/README.md
CHANGED
|
@@ -10,34 +10,42 @@
|
|
|
10
10
|
[`@gullabs/core`](../core) that routes LLM calls through the `claude`
|
|
11
11
|
(Claude Code) CLI instead of an API key. Because the CLI owns its own
|
|
12
12
|
OAuth/keychain-backed session, calls made through this adapter cost $0 in API
|
|
13
|
-
spend
|
|
13
|
+
spend (the runner scrubs `ANTHROPIC_API_KEY` and the other credential variables from
|
|
14
|
+
the child's environment so that stays true; see below) — the CLI reports its own cost for observability, but that number is
|
|
14
15
|
never fed into `@gullabs/core`'s cost engine (`Cost.microUsd` naturally
|
|
15
16
|
resolves to `null` because these models are unpriced).
|
|
16
17
|
|
|
17
18
|
## Install
|
|
18
19
|
|
|
19
20
|
```sh
|
|
20
|
-
pnpm add -D @gullabs/claude-cli
|
|
21
|
+
pnpm add -D @gullabs/claude-cli @gullabs/core
|
|
21
22
|
```
|
|
22
23
|
|
|
23
24
|
## Key exports
|
|
24
25
|
|
|
25
26
|
| Export | Kind | Description |
|
|
26
27
|
| --------------------------- | -------- | --------------------------------------------------------------- |
|
|
28
|
+
| `claudeCliProvider` | function | The `ProviderPlugin` for `composeProviders` (adapter + models). |
|
|
27
29
|
| `claudeCliAdapter` | function | Creates the `ProviderAdapter` (`id: 'claude-cli'`). |
|
|
28
|
-
| `ClaudeCliAdapterOptions` | type | `{ runner?, claudePath?, maxConcurrency? }`.
|
|
30
|
+
| `ClaudeCliAdapterOptions` | type | `{ runner?, claudePath?, maxConcurrency?, env? }`. |
|
|
29
31
|
| `buildClaudeCliRunner` | function | The real `node:child_process`-backed `ClaudeCliRunner` factory. |
|
|
30
32
|
| `ClaudeCliRunner` | type | The process-execution seam; inject a fake in tests. |
|
|
33
|
+
| `ClaudeCliRunOptions` | type | `{ cwd, timeoutMs?, signal?, env? }`, the options of `run`. |
|
|
34
|
+
| `ClaudeCliRunResult` | type | `{ stdout, stderr, exitCode }`. |
|
|
35
|
+
| `ClaudeCliEnvelope` | type | The `--output-format json` result envelope the adapter reads. |
|
|
31
36
|
| `claudeCliModelDescriptors` | value | `ModelDescriptor[]` for the 4 supported model ids. |
|
|
32
37
|
| `claudeCliRegistry` | value | `ModelRegistry` built from `claudeCliModelDescriptors`. |
|
|
38
|
+
| `CLAUDE_CLI_MODEL_IDS` | value | The four registered model ids. |
|
|
39
|
+
| `CLAUDE_CLI_EFFORTS` | value | `['low', 'medium', 'high', 'xhigh', 'max']`. |
|
|
40
|
+
| `Claude…ConfigSchema` (x4) | value | The strict zod config schema of each model. |
|
|
33
41
|
|
|
34
42
|
## Quick example
|
|
35
43
|
|
|
36
44
|
```ts
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
45
|
+
import { composeProviders, createClient } from '@gullabs/core'
|
|
46
|
+
import { claudeCliProvider } from '@gullabs/claude-cli'
|
|
39
47
|
|
|
40
|
-
const client = createClient({
|
|
48
|
+
const client = createClient({ ...composeProviders([claudeCliProvider()]) })
|
|
41
49
|
|
|
42
50
|
const result = await client.generate(
|
|
43
51
|
{
|
|
@@ -77,16 +85,70 @@ the requested id and no other model. Any other model id throws
|
|
|
77
85
|
`stop_reason: "refusal"` returns `finishReason: 'content_filter'` and preserves
|
|
78
86
|
its billed usage; an error envelope throws `content_filter`.
|
|
79
87
|
|
|
80
|
-
|
|
88
|
+
The live capture of 2026-09-26 (Claude Code 2.1.282) shows it: the invariant argv
|
|
81
89
|
and `--json-schema` completed for all four registered ids, and each success
|
|
82
90
|
envelope contained only its requested id as a `modelUsage` key. The sanitized
|
|
83
91
|
responses are in `src/__fixtures__/model-refresh-p-a1.json`. The full installed CLI version and
|
|
84
92
|
envelope shape are runtime dependencies; an absent `modelUsage` fails closed.
|
|
85
93
|
|
|
94
|
+
Usage follows Anthropic's accounting, in which `input_tokens` excludes both cache lanes: `inputTokens` is
|
|
95
|
+
`input_tokens + cache_read_input_tokens + cache_creation_input_tokens`, `cachedInputTokens` is the cache-read
|
|
96
|
+
part, `details.cacheWrite` is the cache-creation part, and `thinkingTokens` is
|
|
97
|
+
`output_tokens_details.thinking_tokens` (inside `outputTokens`). The adapter is unpriced, so none of this
|
|
98
|
+
becomes a cost.
|
|
99
|
+
|
|
86
100
|
`--model`, `--effort`, `--system-prompt`, and `--json-schema` are appended
|
|
87
101
|
from the request when applicable; the prompt itself is always sent over
|
|
88
102
|
stdin, never as a positional argv entry.
|
|
89
103
|
|
|
104
|
+
`output.jsonSchema` is passed to `--json-schema` as written. Unlike the Google and
|
|
105
|
+
xAI adapters (ADR-034), this adapter does not check the schema against a keyword
|
|
106
|
+
profile: `nullable`, uppercase type names and keywords the CLI may ignore are not
|
|
107
|
+
rejected here, so validate the result yourself.
|
|
108
|
+
|
|
109
|
+
## Environment: the subscription login, never an API key
|
|
110
|
+
|
|
111
|
+
Claude Code's own documentation says `ANTHROPIC_API_KEY` is "used instead of your
|
|
112
|
+
Claude Pro, Max, Team, or Enterprise subscription even if you are logged in. In
|
|
113
|
+
non-interactive mode (`-p`), the key is always used when present"
|
|
114
|
+
(<https://code.claude.com/docs/en/env-vars>), and the adapter always runs `-p`. A
|
|
115
|
+
host that has the key exported for an Anthropic SDK would therefore have every call
|
|
116
|
+
billed to it while the ledger books the call as unpriced, and nothing in the result
|
|
117
|
+
would say so. So the real runner does not hand the host environment to the child. It
|
|
118
|
+
builds an allowlisted copy of `process.env`: `PATH`, `HOME`, `USER`, `LOGNAME`,
|
|
119
|
+
`LANG`/`LC_*`, `TERM`, `TZ`, `TMPDIR`/`TEMP`/`TMP`, `SHELL`, `XDG_*`, the Windows
|
|
120
|
+
profile variables, the proxy variables (`HTTPS_PROXY`, `HTTP_PROXY`, `ALL_PROXY`,
|
|
121
|
+
`NO_PROXY`, either case), `NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`/`SSL_CERT_DIR`, and
|
|
122
|
+
the CLI's own documented settings for a subscription login: `CLAUDE_CONFIG_DIR`,
|
|
123
|
+
`CLAUDE_CODE_OAUTH_TOKEN` (the long-lived subscription token from
|
|
124
|
+
`claude setup-token`) and the mTLS variables `CLAUDE_CODE_CLIENT_CERT`,
|
|
125
|
+
`CLAUDE_CODE_CLIENT_KEY`, `CLAUDE_CODE_CLIENT_KEY_PASSPHRASE`. `ANTHROPIC_API_KEY`,
|
|
126
|
+
`ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_BASE_URL`, `CLAUDE_CODE_USE_BEDROCK`/`_VERTEX`/
|
|
127
|
+
`_FOUNDRY` and everything else are dropped.
|
|
128
|
+
|
|
129
|
+
The list does keep credentials: the library never interprets an environment credential,
|
|
130
|
+
but `CLAUDE_CODE_OAUTH_TOKEN` and the mTLS variables `CLAUDE_CODE_CLIENT_CERT`,
|
|
131
|
+
`CLAUDE_CODE_CLIENT_KEY` and `CLAUDE_CODE_CLIENT_KEY_PASSPHRASE` are forwarded from the
|
|
132
|
+
host environment to the CLI, so a host that has them exported is using them (and a proxy
|
|
133
|
+
URL in `HTTPS_PROXY` can carry credentials too). `env` adds to the copy and cannot remove
|
|
134
|
+
from it: to keep the CLI off an ambient token, unset the variable before the call.
|
|
135
|
+
|
|
136
|
+
`claudeCliAdapter({ env })` adds variables on top, and they win over the allowlisted
|
|
137
|
+
ones. It is the one way to pass anything else on. Putting an API key there is the
|
|
138
|
+
explicit opt-in: the call is then billed to it. `env` is validated at construction
|
|
139
|
+
(string names without `=`, string values without NUL), else `bad_request`. A custom
|
|
140
|
+
runner receives it as `ClaudeCliRunOptions.env` and decides for itself.
|
|
141
|
+
|
|
142
|
+
## Queued calls and killed processes
|
|
143
|
+
|
|
144
|
+
A call waits for a slot before it makes its scratch directory, and leaves the queue
|
|
145
|
+
(rejecting `aborted`) when its signal fires, so calls the engine already gave up on
|
|
146
|
+
cost nothing while they wait. When a timeout, abort or the stdout cap kills the call,
|
|
147
|
+
the whole process group gets SIGTERM, and SIGKILL after five seconds. When the CLI
|
|
148
|
+
itself exits on the SIGTERM the group gets one more SIGKILL at that moment, so a tool
|
|
149
|
+
process that ignored SIGTERM and holds none of the runner's pipes does not outlive
|
|
150
|
+
the call.
|
|
151
|
+
|
|
90
152
|
## Concurrency
|
|
91
153
|
|
|
92
154
|
The adapter caps concurrent `claude` CLI invocations with an internal
|
|
@@ -102,3 +164,7 @@ xhigh | max`. Haiku 4.5 has no `reasoning` key — the CLI drops `--effort`.
|
|
|
102
164
|
No sampling knobs (`temperature`/`topP`/`topK`/`maxOutputTokens`/`stopSequences`)
|
|
103
165
|
are accepted; the strict config schema rejects unknown keys. `claude-fable-5`
|
|
104
166
|
and `claude-opus-4-8` are deleted with no alias.
|
|
167
|
+
|
|
168
|
+
The descriptors' `limits` are what the CLI reports for its own run (the captured
|
|
169
|
+
`modelUsage.contextWindow` and `maxOutputTokens`), not the API maxima: Fable 5.1
|
|
170
|
+
64 000 and Haiku 4.5 32 000 output tokens, Opus 5.5 and Sonnet 5 128 000.
|
package/dist/index.cjs
CHANGED
|
@@ -5,9 +5,107 @@ var os = require('os');
|
|
|
5
5
|
var path = require('path');
|
|
6
6
|
var core = require('@gullabs/core');
|
|
7
7
|
var child_process = require('child_process');
|
|
8
|
+
var string_decoder = require('string_decoder');
|
|
8
9
|
var zod = require('zod');
|
|
9
10
|
|
|
10
11
|
// src/adapter.ts
|
|
12
|
+
var INHERITED_NAMES = /* @__PURE__ */ new Set([
|
|
13
|
+
// Process basics.
|
|
14
|
+
"PATH",
|
|
15
|
+
"HOME",
|
|
16
|
+
"USER",
|
|
17
|
+
"LOGNAME",
|
|
18
|
+
"LANG",
|
|
19
|
+
"LANGUAGE",
|
|
20
|
+
"TERM",
|
|
21
|
+
"TMPDIR",
|
|
22
|
+
"TEMP",
|
|
23
|
+
"TMP",
|
|
24
|
+
"SHELL",
|
|
25
|
+
"TZ",
|
|
26
|
+
// Windows equivalents of the above.
|
|
27
|
+
"SYSTEMROOT",
|
|
28
|
+
"SYSTEMDRIVE",
|
|
29
|
+
"WINDIR",
|
|
30
|
+
"COMSPEC",
|
|
31
|
+
"PATHEXT",
|
|
32
|
+
"USERPROFILE",
|
|
33
|
+
"APPDATA",
|
|
34
|
+
"LOCALAPPDATA",
|
|
35
|
+
"PROGRAMDATA",
|
|
36
|
+
"HOMEDRIVE",
|
|
37
|
+
"HOMEPATH",
|
|
38
|
+
// Network: proxies and trust roots.
|
|
39
|
+
"HTTPS_PROXY",
|
|
40
|
+
"HTTP_PROXY",
|
|
41
|
+
"ALL_PROXY",
|
|
42
|
+
"NO_PROXY",
|
|
43
|
+
"https_proxy",
|
|
44
|
+
"http_proxy",
|
|
45
|
+
"all_proxy",
|
|
46
|
+
"no_proxy",
|
|
47
|
+
"NODE_EXTRA_CA_CERTS",
|
|
48
|
+
"SSL_CERT_FILE",
|
|
49
|
+
"SSL_CERT_DIR",
|
|
50
|
+
// The CLI's own documented settings that keep the subscription login usable:
|
|
51
|
+
// where its login lives, the long-lived subscription token from
|
|
52
|
+
// `claude setup-token`, and client certificates for an mTLS proxy.
|
|
53
|
+
"CLAUDE_CONFIG_DIR",
|
|
54
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
55
|
+
"CLAUDE_CODE_CLIENT_CERT",
|
|
56
|
+
"CLAUDE_CODE_CLIENT_KEY",
|
|
57
|
+
"CLAUDE_CODE_CLIENT_KEY_PASSPHRASE",
|
|
58
|
+
"CLAUDE_CODE_PROXY_RESOLVES_HOSTS"
|
|
59
|
+
]);
|
|
60
|
+
var INHERITED_PREFIXES = ["LC_", "XDG_"];
|
|
61
|
+
function isInherited(name, windows) {
|
|
62
|
+
const key = windows ? name.toUpperCase() : name;
|
|
63
|
+
if (INHERITED_NAMES.has(key)) return true;
|
|
64
|
+
return INHERITED_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
65
|
+
}
|
|
66
|
+
function buildChildEnv(parent, extra, windows) {
|
|
67
|
+
const env = {};
|
|
68
|
+
for (const [name, value] of Object.entries(parent)) {
|
|
69
|
+
if (value !== void 0 && isInherited(name, windows)) env[name] = value;
|
|
70
|
+
}
|
|
71
|
+
if (extra !== void 0) {
|
|
72
|
+
if (windows) {
|
|
73
|
+
const overridden = new Set(Object.keys(extra).map((name) => name.toUpperCase()));
|
|
74
|
+
for (const name of Object.keys(env)) {
|
|
75
|
+
if (overridden.has(name.toUpperCase())) delete env[name];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
Object.assign(env, extra);
|
|
79
|
+
}
|
|
80
|
+
return env;
|
|
81
|
+
}
|
|
82
|
+
function badEnv(message) {
|
|
83
|
+
return new core.LlmError(message, {
|
|
84
|
+
kind: "bad_request",
|
|
85
|
+
retryable: false,
|
|
86
|
+
provider: "claude-cli"
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function parseExtraEnv(env) {
|
|
90
|
+
if (env === void 0) return void 0;
|
|
91
|
+
if (env === null || typeof env !== "object" || Array.isArray(env)) {
|
|
92
|
+
throw badEnv("claudeCliAdapter: `env` must be an object of string values");
|
|
93
|
+
}
|
|
94
|
+
const copy = {};
|
|
95
|
+
for (const [name, value] of Object.entries(env)) {
|
|
96
|
+
if (name === "" || name.includes("=") || name.includes("\0")) {
|
|
97
|
+
throw badEnv(`claudeCliAdapter: \`env\` has an invalid name "${name}"`);
|
|
98
|
+
}
|
|
99
|
+
if (typeof value !== "string" || value.includes("\0")) {
|
|
100
|
+
throw badEnv(
|
|
101
|
+
`claudeCliAdapter: \`env.${name}\` must be a string without NUL characters`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
copy[name] = value;
|
|
105
|
+
}
|
|
106
|
+
return Object.freeze(copy);
|
|
107
|
+
}
|
|
108
|
+
var MAX_STDOUT_BYTES = 32 * 1024 * 1024;
|
|
11
109
|
function buildClaudeCliRunner(claudePath = "claude") {
|
|
12
110
|
return {
|
|
13
111
|
run(args, input, opts) {
|
|
@@ -20,9 +118,14 @@ function buildClaudeCliRunner(claudePath = "claude") {
|
|
|
20
118
|
}
|
|
21
119
|
const child = child_process.spawn(claudePath, args, {
|
|
22
120
|
cwd: opts.cwd,
|
|
23
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
121
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
122
|
+
detached: process.platform !== "win32",
|
|
123
|
+
env: buildChildEnv(process.env, opts.env, process.platform === "win32")
|
|
24
124
|
});
|
|
125
|
+
const stdoutDecoder = new string_decoder.StringDecoder("utf8");
|
|
126
|
+
const stderrDecoder = new string_decoder.StringDecoder("utf8");
|
|
25
127
|
let stdout = "";
|
|
128
|
+
let stdoutBytes = 0;
|
|
26
129
|
let stderr = "";
|
|
27
130
|
let settled = false;
|
|
28
131
|
let pendingError;
|
|
@@ -38,12 +141,31 @@ function buildClaudeCliRunner(claudePath = "claude") {
|
|
|
38
141
|
opts.signal.removeEventListener("abort", onAbort);
|
|
39
142
|
}
|
|
40
143
|
};
|
|
144
|
+
const signalTree = (signal) => {
|
|
145
|
+
if (process.platform !== "win32" && child.pid !== void 0) {
|
|
146
|
+
try {
|
|
147
|
+
process.kill(-child.pid, signal);
|
|
148
|
+
return;
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
child.kill(signal);
|
|
153
|
+
};
|
|
41
154
|
const killChild = () => {
|
|
42
|
-
|
|
155
|
+
signalTree("SIGTERM");
|
|
43
156
|
killTimeoutHandle = setTimeout(() => {
|
|
44
|
-
|
|
157
|
+
signalTree("SIGKILL");
|
|
158
|
+
child.stdout.destroy();
|
|
159
|
+
child.stderr.destroy();
|
|
45
160
|
}, 5e3);
|
|
46
161
|
};
|
|
162
|
+
const sweepGroup = () => {
|
|
163
|
+
if (process.platform === "win32" || child.pid === void 0) return;
|
|
164
|
+
try {
|
|
165
|
+
process.kill(-child.pid, "SIGKILL");
|
|
166
|
+
} catch {
|
|
167
|
+
}
|
|
168
|
+
};
|
|
47
169
|
const beginReject = (err) => {
|
|
48
170
|
if (settled || pendingError !== void 0) return;
|
|
49
171
|
pendingError = err;
|
|
@@ -66,10 +188,22 @@ function buildClaudeCliRunner(claudePath = "claude") {
|
|
|
66
188
|
}, opts.timeoutMs);
|
|
67
189
|
}
|
|
68
190
|
child.stdout.on("data", (chunk) => {
|
|
69
|
-
|
|
191
|
+
if (pendingError !== void 0) return;
|
|
192
|
+
stdoutBytes += chunk.length;
|
|
193
|
+
if (stdoutBytes > MAX_STDOUT_BYTES) {
|
|
194
|
+
const err = new Error(
|
|
195
|
+
`claude-cli stdout exceeded ${MAX_STDOUT_BYTES} bytes; the process was killed`
|
|
196
|
+
);
|
|
197
|
+
err.name = "OutputLimitError";
|
|
198
|
+
beginReject(err);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
stdout += stdoutDecoder.write(chunk);
|
|
70
202
|
});
|
|
71
203
|
child.stderr.on("data", (chunk) => {
|
|
72
|
-
stderr
|
|
204
|
+
stderr = (stderr + stderrDecoder.write(chunk)).slice(-1048576);
|
|
205
|
+
});
|
|
206
|
+
child.stdin.on("error", () => {
|
|
73
207
|
});
|
|
74
208
|
child.once("error", (err) => {
|
|
75
209
|
if (settled) return;
|
|
@@ -82,8 +216,11 @@ function buildClaudeCliRunner(claudePath = "claude") {
|
|
|
82
216
|
settled = true;
|
|
83
217
|
cleanup();
|
|
84
218
|
if (pendingError !== void 0) {
|
|
219
|
+
sweepGroup();
|
|
85
220
|
reject(pendingError);
|
|
86
221
|
} else {
|
|
222
|
+
stdout += stdoutDecoder.end();
|
|
223
|
+
stderr = (stderr + stderrDecoder.end()).slice(-1048576);
|
|
87
224
|
resolve({ stdout, stderr, exitCode });
|
|
88
225
|
}
|
|
89
226
|
});
|
|
@@ -94,31 +231,52 @@ function buildClaudeCliRunner(claudePath = "claude") {
|
|
|
94
231
|
}
|
|
95
232
|
|
|
96
233
|
// src/adapter.ts
|
|
234
|
+
function abortError() {
|
|
235
|
+
const err = new Error("claude-cli call aborted");
|
|
236
|
+
err.name = "AbortError";
|
|
237
|
+
return err;
|
|
238
|
+
}
|
|
97
239
|
var Semaphore = class {
|
|
98
240
|
available;
|
|
99
|
-
|
|
241
|
+
waiters = [];
|
|
100
242
|
constructor(max) {
|
|
101
243
|
this.available = max;
|
|
102
244
|
}
|
|
103
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Take a slot. A caller that has to wait leaves the queue, and rejects with an
|
|
247
|
+
* `AbortError`, as soon as `signal` fires: a call the engine already gave up
|
|
248
|
+
* on must not hold a place in line.
|
|
249
|
+
*/
|
|
250
|
+
acquire(signal) {
|
|
251
|
+
if (signal?.aborted === true) return Promise.reject(abortError());
|
|
104
252
|
if (this.available > 0) {
|
|
105
253
|
this.available -= 1;
|
|
106
|
-
return () => {
|
|
254
|
+
return Promise.resolve(() => {
|
|
107
255
|
this.release();
|
|
108
|
-
};
|
|
256
|
+
});
|
|
109
257
|
}
|
|
110
|
-
|
|
111
|
-
|
|
258
|
+
return new Promise((resolve, reject) => {
|
|
259
|
+
const grant = () => {
|
|
260
|
+
signal?.removeEventListener("abort", onAbort);
|
|
261
|
+
resolve(() => {
|
|
262
|
+
this.release();
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
const onAbort = () => {
|
|
266
|
+
const at = this.waiters.indexOf(grant);
|
|
267
|
+
if (at >= 0) this.waiters.splice(at, 1);
|
|
268
|
+
reject(abortError());
|
|
269
|
+
};
|
|
270
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
271
|
+
this.waiters.push(grant);
|
|
112
272
|
});
|
|
113
|
-
this.available -= 1;
|
|
114
|
-
return () => {
|
|
115
|
-
this.release();
|
|
116
|
-
};
|
|
117
273
|
}
|
|
274
|
+
// A freed slot goes straight to the next waiter, so a new caller cannot
|
|
275
|
+
// overtake it between the release and the waiter's wake-up.
|
|
118
276
|
release() {
|
|
119
|
-
this.
|
|
120
|
-
const next = this.queue.shift();
|
|
277
|
+
const next = this.waiters.shift();
|
|
121
278
|
if (next !== void 0) next();
|
|
279
|
+
else this.available += 1;
|
|
122
280
|
}
|
|
123
281
|
};
|
|
124
282
|
function mapFinishReason(stopReason) {
|
|
@@ -135,14 +293,19 @@ function mapFinishReason(stopReason) {
|
|
|
135
293
|
}
|
|
136
294
|
}
|
|
137
295
|
function mapUsage(usage) {
|
|
138
|
-
const
|
|
296
|
+
const cacheRead = usage?.cache_read_input_tokens;
|
|
297
|
+
const cacheWrite = usage?.cache_creation_input_tokens;
|
|
298
|
+
const inputTokens = (usage?.input_tokens ?? 0) + (cacheRead ?? 0) + (cacheWrite ?? 0);
|
|
139
299
|
const outputTokens = usage?.output_tokens ?? 0;
|
|
140
|
-
const cachedInputTokens =
|
|
300
|
+
const cachedInputTokens = cacheRead;
|
|
301
|
+
const thinkingTokens = usage?.output_tokens_details?.thinking_tokens;
|
|
141
302
|
const totalTokens = usage !== void 0 ? inputTokens + outputTokens : void 0;
|
|
142
303
|
const details = {
|
|
143
304
|
input: inputTokens,
|
|
144
305
|
output: outputTokens,
|
|
145
306
|
...cachedInputTokens !== void 0 ? { cached: cachedInputTokens } : {},
|
|
307
|
+
...cacheWrite !== void 0 ? { cacheWrite } : {},
|
|
308
|
+
...thinkingTokens !== void 0 ? { thinking: thinkingTokens } : {},
|
|
146
309
|
...totalTokens !== void 0 ? { total: totalTokens } : {}
|
|
147
310
|
};
|
|
148
311
|
const raw = usage !== void 0 ? usage : null;
|
|
@@ -152,6 +315,7 @@ function mapUsage(usage) {
|
|
|
152
315
|
details,
|
|
153
316
|
raw,
|
|
154
317
|
...cachedInputTokens !== void 0 ? { cachedInputTokens } : {},
|
|
318
|
+
...thinkingTokens !== void 0 ? { thinkingTokens } : {},
|
|
155
319
|
...totalTokens !== void 0 ? { totalTokens } : {}
|
|
156
320
|
};
|
|
157
321
|
}
|
|
@@ -181,11 +345,13 @@ function buildPrompt(messages) {
|
|
|
181
345
|
${messageText(msg)}`;
|
|
182
346
|
}).join("\n\n");
|
|
183
347
|
}
|
|
348
|
+
var AUTH_FAILURE = /\b(?:log ?in|auth|oauth|authentication|unauthori[sz]ed|401)\b/i;
|
|
349
|
+
var RATE_LIMITED = /\brate[ -]?limit(?:ed|s|ing)?\b|\btoo many requests\b|(?<![\w.:/-])429(?![\w-]|\.\d)/i;
|
|
184
350
|
function looksAuthy(text) {
|
|
185
|
-
return
|
|
351
|
+
return AUTH_FAILURE.test(text);
|
|
186
352
|
}
|
|
187
353
|
function looksRateLimited(text) {
|
|
188
|
-
return
|
|
354
|
+
return RATE_LIMITED.test(text);
|
|
189
355
|
}
|
|
190
356
|
function assertServedModel(envelope, requestedModel) {
|
|
191
357
|
const modelUsage = envelope.modelUsage;
|
|
@@ -220,12 +386,6 @@ function classifyRunFailure(envelope, result) {
|
|
|
220
386
|
});
|
|
221
387
|
}
|
|
222
388
|
const combinedText = `${envelope?.subtype ?? ""} ${result.stderr}`;
|
|
223
|
-
if (looksAuthy(combinedText)) {
|
|
224
|
-
return new core.LlmError(
|
|
225
|
-
`claude CLI reported an authentication failure: ${result.stderr.slice(-500)}`,
|
|
226
|
-
{ kind: "invalid_auth", retryable: false, provider: "claude-cli" }
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
389
|
if (looksRateLimited(combinedText)) {
|
|
230
390
|
return new core.LlmError(
|
|
231
391
|
`claude CLI reported rate limiting: ${result.stderr.slice(-500)}`,
|
|
@@ -236,6 +396,12 @@ function classifyRunFailure(envelope, result) {
|
|
|
236
396
|
}
|
|
237
397
|
);
|
|
238
398
|
}
|
|
399
|
+
if (looksAuthy(combinedText)) {
|
|
400
|
+
return new core.LlmError(
|
|
401
|
+
`claude CLI reported an authentication failure: ${result.stderr.slice(-500)}`,
|
|
402
|
+
{ kind: "invalid_auth", retryable: false, provider: "claude-cli" }
|
|
403
|
+
);
|
|
404
|
+
}
|
|
239
405
|
if (envelope?.is_error === true) {
|
|
240
406
|
return new core.LlmError(
|
|
241
407
|
`claude CLI reported an error (subtype: ${envelope.subtype ?? "unknown"}): ${result.stderr.slice(-500)}`,
|
|
@@ -251,6 +417,7 @@ function classifyRunFailure(envelope, result) {
|
|
|
251
417
|
function claudeCliAdapter(opts) {
|
|
252
418
|
const runner = opts?.runner ?? buildClaudeCliRunner(opts?.claudePath);
|
|
253
419
|
const semaphore = new Semaphore(opts?.maxConcurrency ?? 2);
|
|
420
|
+
const env = parseExtraEnv(opts?.env);
|
|
254
421
|
return {
|
|
255
422
|
id: "claude-cli",
|
|
256
423
|
async run(req, ctx) {
|
|
@@ -322,14 +489,29 @@ function claudeCliAdapter(opts) {
|
|
|
322
489
|
args.push("--json-schema", JSON.stringify(req.outputJsonSchema));
|
|
323
490
|
}
|
|
324
491
|
const timeoutMs = req.attemptTimeoutMs ?? config.timeoutMs;
|
|
325
|
-
|
|
326
|
-
|
|
492
|
+
let release;
|
|
493
|
+
try {
|
|
494
|
+
release = await semaphore.acquire(ctx.signal);
|
|
495
|
+
} catch (waitErr) {
|
|
496
|
+
throw new core.LlmError(
|
|
497
|
+
waitErr instanceof Error ? waitErr.message : "claude-cli call aborted",
|
|
498
|
+
{ kind: "aborted", retryable: false, provider: "claude-cli" }
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
let cwd;
|
|
502
|
+
try {
|
|
503
|
+
cwd = await promises.mkdtemp(path.join(os.tmpdir(), "claude-cli-"));
|
|
504
|
+
} catch (mkdirErr) {
|
|
505
|
+
release();
|
|
506
|
+
throw mkdirErr;
|
|
507
|
+
}
|
|
327
508
|
let result;
|
|
328
509
|
try {
|
|
329
510
|
result = await runner.run(args, prompt, {
|
|
330
511
|
cwd,
|
|
331
512
|
...timeoutMs !== void 0 ? { timeoutMs } : {},
|
|
332
|
-
...ctx.signal !== void 0 ? { signal: ctx.signal } : {}
|
|
513
|
+
...ctx.signal !== void 0 ? { signal: ctx.signal } : {},
|
|
514
|
+
...env !== void 0 ? { env } : {}
|
|
333
515
|
});
|
|
334
516
|
} catch (rawErr) {
|
|
335
517
|
release();
|
|
@@ -394,6 +576,10 @@ function claudeCliAdapter(opts) {
|
|
|
394
576
|
};
|
|
395
577
|
const adapterResult = {
|
|
396
578
|
model,
|
|
579
|
+
message: {
|
|
580
|
+
role: "assistant",
|
|
581
|
+
parts: text.length > 0 ? [{ kind: "text", text }] : []
|
|
582
|
+
},
|
|
397
583
|
usage,
|
|
398
584
|
warnings,
|
|
399
585
|
...text.length > 0 ? { text } : {},
|
|
@@ -453,21 +639,30 @@ var CONFIG_SCHEMAS = {
|
|
|
453
639
|
"claude-sonnet-5": ClaudeSonnet5ConfigSchema,
|
|
454
640
|
"claude-haiku-4-5-20251001": ClaudeHaiku45ConfigSchema
|
|
455
641
|
};
|
|
642
|
+
var CLAUDE_CLI_LIMITS = {
|
|
643
|
+
"claude-fable-5-1": { contextWindow: 1e6, maxOutputTokens: 64e3 },
|
|
644
|
+
"claude-opus-5-5": { contextWindow: 1e6, maxOutputTokens: 128e3 },
|
|
645
|
+
"claude-sonnet-5": { contextWindow: 1e6, maxOutputTokens: 128e3 },
|
|
646
|
+
"claude-haiku-4-5-20251001": { contextWindow: 2e5, maxOutputTokens: 32e3 }
|
|
647
|
+
};
|
|
456
648
|
var claudeCliModelDescriptors = CLAUDE_CLI_MODEL_IDS.map(
|
|
457
649
|
(id) => {
|
|
458
650
|
const configSchema = CONFIG_SCHEMAS[id];
|
|
459
651
|
return {
|
|
460
652
|
model: id,
|
|
461
653
|
provider: "claude-cli",
|
|
654
|
+
limits: CLAUDE_CLI_LIMITS[id],
|
|
462
655
|
capabilities: {
|
|
656
|
+
// The CLI runs text-only: the adapter rejects every non-text part.
|
|
657
|
+
inputMimeTypes: [],
|
|
463
658
|
structuredOutput: true,
|
|
464
659
|
nativeStructuredOutput: true,
|
|
465
660
|
...id !== "claude-haiku-4-5-20251001" ? { reasoningApi: "level" } : {},
|
|
466
661
|
admittedReasoningEfforts: id === "claude-haiku-4-5-20251001" ? [] : CLAUDE_CLI_EFFORTS,
|
|
467
|
-
sampling: "fixed"
|
|
468
|
-
vision: false
|
|
662
|
+
sampling: "fixed"
|
|
469
663
|
},
|
|
470
664
|
configSchema,
|
|
665
|
+
configKeys: core.toConfigKeys(configSchema),
|
|
471
666
|
configJsonSchema: core.toConfigJsonSchema(configSchema),
|
|
472
667
|
validateConfig: core.zodToStandardSchema(configSchema)
|
|
473
668
|
};
|