@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 ADDED
@@ -0,0 +1,6 @@
1
+ any-llm
2
+ Copyright 2026 Gul Labs
3
+
4
+ This product includes software developed at Gul Labs (https://github.com/gul-labs).
5
+
6
+ Licensed under the Apache License, Version 2.0. See LICENSE.
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 — the CLI reports its own cost for observability, but that number is
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 { claudeCliAdapter } from '@gullabs/claude-cli'
38
- import { createClient } from '@gullabs/core'
45
+ import { composeProviders, createClient } from '@gullabs/core'
46
+ import { claudeCliProvider } from '@gullabs/claude-cli'
39
47
 
40
- const client = createClient({ adapters: [claudeCliAdapter()] })
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
- P-A1 was captured on 2026-09-26 with Claude Code 2.1.282: the invariant argv
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
- child.kill("SIGTERM");
155
+ signalTree("SIGTERM");
43
156
  killTimeoutHandle = setTimeout(() => {
44
- child.kill("SIGKILL");
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
- stdout += chunk.toString("utf8");
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 += chunk.toString("utf8");
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
- queue = [];
241
+ waiters = [];
100
242
  constructor(max) {
101
243
  this.available = max;
102
244
  }
103
- async acquire() {
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
- await new Promise((resolve) => {
111
- this.queue.push(resolve);
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.available += 1;
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 inputTokens = usage?.input_tokens ?? 0;
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 = usage?.cache_read_input_tokens;
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 /login|auth|unauthorized/i.test(text);
351
+ return AUTH_FAILURE.test(text);
186
352
  }
187
353
  function looksRateLimited(text) {
188
- return /rate limit|429/i.test(text);
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
- const cwd = await promises.mkdtemp(path.join(os.tmpdir(), "claude-cli-"));
326
- const release = await semaphore.acquire();
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
  };