@openafw/openafw 0.5.2 → 0.6.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/dist/bin/afw.js CHANGED
@@ -1,21 +1,21 @@
1
1
  #!/usr/bin/env node
2
- import { AFW_HOME, DAEMON_BASE_URL, DAEMON_PORT, EMPTY_SECRETS, PRICING_CATALOG_CACHE, PRICING_OVERRIDE, atomicWrite, fileExists, getSecret, paths, readSecrets, removeSecret, secretRefs, setSecret } from "../secrets-Bj-gyv53.js";
3
- import { activeProviderFor, mutateToolProviders, readToolProviders, searchBaidu, searchBrave, searchDuckDuckGo } from "../backends-Byh5VYtT.js";
2
+ import { AFW_HOME, DAEMON_BASE_URL, DAEMON_PORT, EMPTY_SECRETS, PRICING_CATALOG_CACHE, PRICING_OVERRIDE, atomicWrite, fileExists, getSecret, paths, readSecrets, removeSecret, secretRefs, setSecret } from "../secrets-evRw4cV3.js";
3
+ import { activeProviderFor, mutateToolProviders, readToolProviders, searchBaidu, searchBrave, searchDuckDuckGo } from "../backends-BmyOv3bJ.js";
4
4
  import { createRequire } from "module";
5
5
  import process$1 from "node:process";
6
- import { execFile, execFileSync, spawn } from "node:child_process";
7
- import { basename, dirname, extname, join } from "node:path";
8
- import { existsSync, readFileSync, realpathSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
9
- import { copyFile, mkdir, open, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
6
+ import { execFileSync, spawn } from "node:child_process";
7
+ import { basename, delimiter, dirname, extname, isAbsolute, join, normalize, sep } from "node:path";
8
+ import { existsSync, mkdirSync, openSync, readFileSync, realpathSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
9
+ import { access, copyFile, mkdir, open, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
10
10
  import { homedir } from "node:os";
11
11
  import { createHash, randomBytes, webcrypto } from "node:crypto";
12
12
  import * as jsonc from "jsonc-parser";
13
+ import { createServer } from "node:http";
14
+ import { Buffer as Buffer$1 } from "node:buffer";
13
15
  import { createInterface } from "node:readline/promises";
14
16
  import { versions } from "process";
15
- import { Buffer as Buffer$1 } from "node:buffer";
16
- import { promisify } from "node:util";
17
17
  import { fileURLToPath } from "node:url";
18
- import { createServer } from "http";
18
+ import { createServer as createServer$1 } from "http";
19
19
  import { Http2ServerRequest, constants } from "http2";
20
20
  import { Readable } from "stream";
21
21
  import crypto from "crypto";
@@ -3248,9 +3248,30 @@ async function daemonHealthy(timeoutMs = 1500) {
3248
3248
 
3249
3249
  //#endregion
3250
3250
  //#region src/cli/launch/daemon-autostart.ts
3251
- function sleep$1(ms) {
3251
+ function sleep$2(ms) {
3252
3252
  return new Promise((resolve) => setTimeout(resolve, ms));
3253
3253
  }
3254
+ /** stdio for the detached daemon — append its stdout/stderr to the same log
3255
+ * files the installed service uses (~/.afw/logs/daemon.{log,err}), so an
3256
+ * on-demand daemon (the `afw codex` / `afw claude` autostart) is just as
3257
+ * inspectable as a serviced one. The logger only writes to stdout/stderr, so
3258
+ * without this the autostart daemon's output goes nowhere. Falls back to
3259
+ * discarding output if the log dir can't be opened (read-only home, etc.) —
3260
+ * losing logs must never block the launch. */
3261
+ function daemonStdio() {
3262
+ try {
3263
+ mkdirSync(paths.logs.dir, { recursive: true });
3264
+ const out = openSync(paths.logs.daemon, "a");
3265
+ const err = openSync(paths.logs.daemonErr, "a");
3266
+ return [
3267
+ "ignore",
3268
+ out,
3269
+ err
3270
+ ];
3271
+ } catch {
3272
+ return "ignore";
3273
+ }
3274
+ }
3254
3275
  /** Start the daemon if it isn't already answering /health, and wait for it to
3255
3276
  * come up. Re-execs this same CLI (`<node> [execArgv] <cli> daemon`) detached
3256
3277
  * so it survives the launcher process. Throws if it never becomes healthy. */
@@ -3263,11 +3284,11 @@ async function ensureDaemonRunning(opts = {}) {
3263
3284
  "daemon"
3264
3285
  ], {
3265
3286
  detached: true,
3266
- stdio: "ignore"
3287
+ stdio: daemonStdio()
3267
3288
  });
3268
3289
  child.unref();
3269
3290
  for (let i = 0; i < 40; i++) {
3270
- await sleep$1(250);
3291
+ await sleep$2(250);
3271
3292
  if (await daemonHealthy()) return;
3272
3293
  }
3273
3294
  throw new Error("afw daemon did not come up — try `afw daemon` in another terminal");
@@ -3693,6 +3714,391 @@ function decideAutoCompactWindow(opts) {
3693
3714
  };
3694
3715
  }
3695
3716
 
3717
+ //#endregion
3718
+ //#region src/core/model-registry.ts
3719
+ const MODEL_REGISTRY_VERSION = 3;
3720
+ /** OpenRouter Fusion caps its panel at 8 models; mirror that. */
3721
+ const MAX_FUSION_PANEL = 8;
3722
+ const EMPTY_REGISTRY = {
3723
+ version: MODEL_REGISTRY_VERSION,
3724
+ providers: [],
3725
+ models: [],
3726
+ combos: []
3727
+ };
3728
+ const MODEL_APIS$2 = [
3729
+ "anthropic-messages",
3730
+ "openai-chat",
3731
+ "openai-responses"
3732
+ ];
3733
+ const MODALITIES$2 = [
3734
+ "text",
3735
+ "audio",
3736
+ "image",
3737
+ "video",
3738
+ "pdf"
3739
+ ];
3740
+ const REASONING_EFFORTS = [
3741
+ "minimal",
3742
+ "low",
3743
+ "medium",
3744
+ "high",
3745
+ "xhigh"
3746
+ ];
3747
+ const GENERATION_PATH_MODES = ["versioned", "direct"];
3748
+ function findProvider(reg, id) {
3749
+ return reg.providers.find((p) => p.id === id);
3750
+ }
3751
+ /** Look up a model by id, optionally scoped to a provider.
3752
+ *
3753
+ * Same model id can exist under multiple providers (e.g. a Xiangxin
3754
+ * model harvested under `hermes/...` and also added by hand under a
3755
+ * custom `og-text` provider). When `providerId` is supplied we match
3756
+ * the exact pair; otherwise we fall back to the first matching id so
3757
+ * legacy callers (and pre-providerId routing-policy entries) still
3758
+ * resolve to *something* instead of breaking. */
3759
+ function findModel(reg, id, providerId) {
3760
+ if (providerId !== void 0) return reg.models.find((m) => m.id === id && m.providerId === providerId);
3761
+ return reg.models.find((m) => m.id === id);
3762
+ }
3763
+ /** A model's effective wire format: its own override, else its provider's. */
3764
+ function resolveApi(reg, model) {
3765
+ return model.api ?? findProvider(reg, model.providerId)?.api;
3766
+ }
3767
+ function findCombo(reg, id) {
3768
+ return reg.combos.find((c) => c.id === id);
3769
+ }
3770
+ function isObj$9(v) {
3771
+ return typeof v === "object" && v !== null && !Array.isArray(v);
3772
+ }
3773
+ function normalizeAuth(raw$1) {
3774
+ if (!isObj$9(raw$1)) return void 0;
3775
+ if (raw$1.kind === "passthrough") return { kind: "passthrough" };
3776
+ if (raw$1.kind === "agent-oauth" && (raw$1.agent === "claude-code" || raw$1.agent === "codex")) return {
3777
+ kind: "agent-oauth",
3778
+ agent: raw$1.agent
3779
+ };
3780
+ if (raw$1.kind === "bearer" && typeof raw$1.valueRef === "string") return {
3781
+ kind: "bearer",
3782
+ valueRef: raw$1.valueRef
3783
+ };
3784
+ if (raw$1.kind === "api-key" && typeof raw$1.header === "string" && raw$1.header !== "" && typeof raw$1.valueRef === "string") return {
3785
+ kind: "api-key",
3786
+ header: raw$1.header,
3787
+ valueRef: raw$1.valueRef
3788
+ };
3789
+ return void 0;
3790
+ }
3791
+ function normalizeProvider(raw$1) {
3792
+ if (!isObj$9(raw$1)) return void 0;
3793
+ const { id, label, baseUrl, api: api$1, origin, seededFrom, reasoningEffort, generationPath } = raw$1;
3794
+ if (typeof id !== "string" || id === "") return void 0;
3795
+ if (typeof baseUrl !== "string" || baseUrl === "") return void 0;
3796
+ if (!MODEL_APIS$2.includes(api$1)) return void 0;
3797
+ const auth = normalizeAuth(raw$1.auth);
3798
+ if (!auth) return void 0;
3799
+ return {
3800
+ id,
3801
+ label: typeof label === "string" ? label : id,
3802
+ baseUrl,
3803
+ api: api$1,
3804
+ auth,
3805
+ origin: origin === "manual" ? "manual" : "seeded",
3806
+ ...typeof seededFrom === "string" ? { seededFrom } : {},
3807
+ ...REASONING_EFFORTS.includes(reasoningEffort) ? { reasoningEffort } : {},
3808
+ ...GENERATION_PATH_MODES.includes(generationPath) ? { generationPath } : {}
3809
+ };
3810
+ }
3811
+ function normalizeCost(raw$1) {
3812
+ if (!isObj$9(raw$1)) return void 0;
3813
+ const { input, output, cacheRead, cacheWrite } = raw$1;
3814
+ if (typeof input !== "number" || typeof output !== "number") return void 0;
3815
+ return {
3816
+ input,
3817
+ output,
3818
+ ...typeof cacheRead === "number" ? { cacheRead } : {},
3819
+ ...typeof cacheWrite === "number" ? { cacheWrite } : {}
3820
+ };
3821
+ }
3822
+ function normalizeModel(raw$1) {
3823
+ if (!isObj$9(raw$1)) return void 0;
3824
+ const { id, providerId, label, api: api$1, origin, contextWindow, maxTokens, reasoningEffort } = raw$1;
3825
+ if (typeof id !== "string" || id === "") return void 0;
3826
+ if (typeof providerId !== "string" || providerId === "") return void 0;
3827
+ const input = Array.isArray(raw$1.input) ? raw$1.input.filter((m) => MODALITIES$2.includes(m)) : [];
3828
+ const cost = normalizeCost(raw$1.cost);
3829
+ return {
3830
+ id,
3831
+ providerId,
3832
+ label: typeof label === "string" ? label : id,
3833
+ ...MODEL_APIS$2.includes(api$1) ? { api: api$1 } : {},
3834
+ input: input.length > 0 ? input : ["text"],
3835
+ ...typeof contextWindow === "number" ? { contextWindow } : {},
3836
+ ...typeof maxTokens === "number" ? { maxTokens } : {},
3837
+ ...REASONING_EFFORTS.includes(reasoningEffort) ? { reasoningEffort } : {},
3838
+ ...cost ? { cost } : {},
3839
+ origin: origin === "manual" ? "manual" : "seeded"
3840
+ };
3841
+ }
3842
+ function normalizeFusionEndpoint(raw$1) {
3843
+ if (!isObj$9(raw$1)) return void 0;
3844
+ if (typeof raw$1.modelId !== "string" || raw$1.modelId === "") return void 0;
3845
+ return {
3846
+ modelId: raw$1.modelId,
3847
+ ...typeof raw$1.providerId === "string" && raw$1.providerId !== "" ? { providerId: raw$1.providerId } : {}
3848
+ };
3849
+ }
3850
+ function normalizeWebSearch(raw$1) {
3851
+ if (!isObj$9(raw$1)) return void 0;
3852
+ return typeof raw$1.providerId === "string" && raw$1.providerId !== "" ? { providerId: raw$1.providerId } : {};
3853
+ }
3854
+ /** A fusion panel member: the primary model, its per-member failover rules
3855
+ * (`switchOn` — token/USD caps + error), and the `fallback` model they switch
3856
+ * to. `normalizeMember` parses the {modelId, providerId, switchOn} part. */
3857
+ function normalizeFusionMember(raw$1) {
3858
+ const base = normalizeMember(raw$1);
3859
+ if (!base) return void 0;
3860
+ const fallback = isObj$9(raw$1) ? normalizeFusionEndpoint(raw$1.fallback) : void 0;
3861
+ return {
3862
+ modelId: base.modelId,
3863
+ ...base.providerId ? { providerId: base.providerId } : {},
3864
+ ...base.switchOn ? { switchOn: base.switchOn } : {},
3865
+ ...fallback ? { fallback } : {}
3866
+ };
3867
+ }
3868
+ /** Lift a legacy failover-combo's combo-level vision/web_search capabilities to
3869
+ * the fusion's combo-level vision/web_search, so they survive the move from
3870
+ * failover chains to fusion. */
3871
+ function legacyCaps(rawCaps) {
3872
+ const caps = normalizeCapabilities(rawCaps);
3873
+ const vision = caps?.vision?.via === "companion" ? {
3874
+ modelId: caps.vision.modelId,
3875
+ ...caps.vision.providerId ? { providerId: caps.vision.providerId } : {}
3876
+ } : void 0;
3877
+ const webSearch = caps?.web_search?.via === "local" ? caps.web_search.providerId ? { providerId: caps.web_search.providerId } : {} : void 0;
3878
+ return {
3879
+ ...vision ? { vision } : {},
3880
+ ...webSearch ? { webSearch } : {}
3881
+ };
3882
+ }
3883
+ /** A fusion combination model. Accepts the current `panel` shape and migrates
3884
+ * the legacy failover-combo shape (`members` + `capabilities`) forward: each
3885
+ * member becomes a panel member, and the old combo-level vision/web_search
3886
+ * capabilities become the combo-level `vision`/`webSearch`. Dropped when it has
3887
+ * no id or no resolvable panel member. */
3888
+ function normalizeCombo(raw$1) {
3889
+ if (!isObj$9(raw$1)) return void 0;
3890
+ const { id, label } = raw$1;
3891
+ if (typeof id !== "string" || id === "") return void 0;
3892
+ let panel;
3893
+ let migrated = {};
3894
+ if (Array.isArray(raw$1.panel)) panel = raw$1.panel.map(normalizeFusionMember).filter((m) => m != null);
3895
+ else if (Array.isArray(raw$1.members)) {
3896
+ migrated = legacyCaps(raw$1.capabilities);
3897
+ panel = raw$1.members.map(normalizeMember).filter((m) => m != null).map((m) => ({
3898
+ modelId: m.modelId,
3899
+ ...m.providerId ? { providerId: m.providerId } : {},
3900
+ ...m.switchOn ? { switchOn: m.switchOn } : {}
3901
+ }));
3902
+ } else panel = [];
3903
+ if (panel.length === 0) return void 0;
3904
+ const vision = normalizeFusionEndpoint(raw$1.vision) ?? migrated.vision;
3905
+ const webSearch = normalizeWebSearch(raw$1.webSearch) ?? migrated.webSearch;
3906
+ const judge = normalizeFusionEndpoint(raw$1.judge);
3907
+ const synthesizer = normalizeFusionEndpoint(raw$1.synthesizer);
3908
+ const cheapModel = normalizeFusionEndpoint(raw$1.cheapModel);
3909
+ return {
3910
+ id,
3911
+ label: typeof label === "string" && label !== "" ? label : id,
3912
+ panel: panel.slice(0, MAX_FUSION_PANEL),
3913
+ ...vision ? { vision } : {},
3914
+ ...webSearch ? { webSearch } : {},
3915
+ ...judge ? { judge } : {},
3916
+ ...synthesizer ? { synthesizer } : {},
3917
+ ...cheapModel ? { cheapModel } : {},
3918
+ origin: "manual"
3919
+ };
3920
+ }
3921
+ /** Coerce parsed JSON into a valid registry — malformed entries are dropped,
3922
+ * a hand-edited file with one bad entry never wipes the rest. v1 (no `combos`)
3923
+ * and v2 (failover-style combos) are accepted and migrated forward by
3924
+ * `normalizeCombo` — v2 combos' `members`/`capabilities` become a fusion
3925
+ * `panel`. Throws only on an unsupported version, so a future format is never
3926
+ * silently downgraded. */
3927
+ function normalizeModelRegistry(raw$1) {
3928
+ if (!isObj$9(raw$1)) return { ...EMPTY_REGISTRY };
3929
+ if (raw$1.version !== 1 && raw$1.version !== 2 && raw$1.version !== MODEL_REGISTRY_VERSION) throw new Error(`models.json version ${String(raw$1.version)} not supported (expected ${MODEL_REGISTRY_VERSION})`);
3930
+ const providers = Array.isArray(raw$1.providers) ? raw$1.providers.map(normalizeProvider).filter((p) => p != null) : [];
3931
+ const models = Array.isArray(raw$1.models) ? raw$1.models.map(normalizeModel).filter((m) => m != null) : [];
3932
+ const combos = Array.isArray(raw$1.combos) ? raw$1.combos.map(normalizeCombo).filter((c) => c != null) : [];
3933
+ return {
3934
+ version: MODEL_REGISTRY_VERSION,
3935
+ providers,
3936
+ models,
3937
+ combos
3938
+ };
3939
+ }
3940
+ async function readModelRegistry() {
3941
+ if (!await fileExists(paths.models)) return { ...EMPTY_REGISTRY };
3942
+ return normalizeModelRegistry(JSON.parse(await readFile(paths.models, "utf8")));
3943
+ }
3944
+ async function writeModelRegistry(reg) {
3945
+ await atomicWrite(paths.models, `${JSON.stringify(reg, null, 2)}\n`);
3946
+ }
3947
+ let writeChain$3 = Promise.resolve();
3948
+ function mutateModelRegistry(fn) {
3949
+ const next = writeChain$3.then(async () => {
3950
+ const reg = await readModelRegistry();
3951
+ const updated = fn(reg);
3952
+ if (updated) await writeModelRegistry(updated);
3953
+ return updated ?? reg;
3954
+ });
3955
+ writeChain$3 = next.catch(() => {});
3956
+ return next;
3957
+ }
3958
+
3959
+ //#endregion
3960
+ //#region src/cli/launch/codex-protocol.ts
3961
+ /** Resolve codex's effective backend wire format. `modelOverride` is the
3962
+ * per-launch `--model` (a routed instance pins one model); without it the
3963
+ * type-level `codex/*` routing default decides. Best-effort: any read failure
3964
+ * or an unresolved/mixed target falls back to Responses (codex's native). */
3965
+ async function resolveCodexWireProtocol(modelOverride) {
3966
+ const api$1 = await resolveBackendApi(modelOverride).catch(() => void 0);
3967
+ if (api$1 === "openai-chat") return {
3968
+ wireApi: "chat",
3969
+ decoder: "openai-chat"
3970
+ };
3971
+ return {
3972
+ wireApi: "responses",
3973
+ decoder: "openai-responses"
3974
+ };
3975
+ }
3976
+ async function resolveBackendApi(modelOverride) {
3977
+ const reg = await readModelRegistry();
3978
+ if (modelOverride) {
3979
+ const m = findModel(reg, modelOverride);
3980
+ if (m) return resolveApi(reg, m);
3981
+ }
3982
+ const policy$1 = await readRoutingPolicy();
3983
+ const target = policy$1.agents["codex/*"]?.target ?? policy$1.agents.codex?.target;
3984
+ if (target?.kind === "chain") {
3985
+ const first = target.members[0];
3986
+ if (first) {
3987
+ const m = findModel(reg, first.modelId, first.providerId);
3988
+ if (m) return resolveApi(reg, m);
3989
+ }
3990
+ }
3991
+ return "openai-responses";
3992
+ }
3993
+
3994
+ //#endregion
3995
+ //#region src/cli/launch/resolve-bin.ts
3996
+ const EXECUTABLE_EXTENSIONS = [".exe", ".com"];
3997
+ const SHELL_EXTENSIONS = [".cmd", ".bat"];
3998
+ async function exists$1(path$1) {
3999
+ try {
4000
+ await access(path$1);
4001
+ return true;
4002
+ } catch {
4003
+ return false;
4004
+ }
4005
+ }
4006
+ function pathValue(env) {
4007
+ return env.PATH ?? env.Path ?? env.path ?? "";
4008
+ }
4009
+ function pathExts(env) {
4010
+ const raw$1 = env.PATHEXT ?? env.PathExt ?? ".COM;.EXE;.BAT;.CMD";
4011
+ return raw$1.split(";").map((x) => x.trim().toLowerCase()).filter(Boolean);
4012
+ }
4013
+ function hasPathSeparator(command) {
4014
+ return command.includes("/") || command.includes("\\");
4015
+ }
4016
+ /** Resolve a candidate path the way Windows' loader does — case-insensitively.
4017
+ * Returns the real on-disk path (preserving its actual case) or undefined.
4018
+ * An exact match is the fast path (correct on real Windows and on a
4019
+ * case-insensitive host FS like macOS); the directory scan is the fallback
4020
+ * that makes win32 resolution behave correctly even on a case-sensitive host
4021
+ * FS (Linux CI), where `PATHEXT`'s case need not match the file's. */
4022
+ async function resolveCaseInsensitive(path$1) {
4023
+ if (await exists$1(path$1)) return path$1;
4024
+ const dir = dirname(path$1);
4025
+ const target = basename(path$1).toLowerCase();
4026
+ try {
4027
+ const entries = await readdir(dir);
4028
+ const match$1 = entries.find((e) => e.toLowerCase() === target);
4029
+ return match$1 ? join(dir, match$1) : void 0;
4030
+ } catch {
4031
+ return void 0;
4032
+ }
4033
+ }
4034
+ async function firstExisting(candidates) {
4035
+ for (const candidate of candidates) {
4036
+ const hit = await resolveCaseInsensitive(candidate);
4037
+ if (hit) return hit;
4038
+ }
4039
+ return void 0;
4040
+ }
4041
+ function asResolved(command, shell, argsPrefix = []) {
4042
+ return {
4043
+ command,
4044
+ argsPrefix,
4045
+ shell
4046
+ };
4047
+ }
4048
+ function windowsCandidates(command, env) {
4049
+ const dirs = hasPathSeparator(command) || isAbsolute(command) ? [""] : pathValue(env).split(delimiter);
4050
+ const exts = pathExts(env);
4051
+ const hasExt = /\.[^\\/]+$/.test(command);
4052
+ const suffixes = hasExt ? [""] : [...EXECUTABLE_EXTENSIONS.filter((ext) => exts.includes(ext)), ...SHELL_EXTENSIONS.filter((ext) => exts.includes(ext))];
4053
+ const out = [];
4054
+ for (const dir of dirs) {
4055
+ if (!dir && !hasPathSeparator(command) && !isAbsolute(command)) continue;
4056
+ for (const suffix of suffixes) out.push(dir ? join(dir, `${command}${suffix}`) : `${command}${suffix}`);
4057
+ }
4058
+ return out;
4059
+ }
4060
+ function resolveShimPath(raw$1, baseDir) {
4061
+ const withBaseDir = raw$1.replace(/%~dp0/gi, baseDir).replace(/%dp0%/gi, baseDir);
4062
+ const withNativeSeparators = withBaseDir.replace(/[\\/]+/g, sep);
4063
+ return normalize(isAbsolute(withNativeSeparators) ? withNativeSeparators : join(baseDir, withNativeSeparators));
4064
+ }
4065
+ function quotedDp0Targets(text$1, baseDir) {
4066
+ return [...text$1.matchAll(/"([^"]*%~?dp0[^"]*)"/gi)].map((match$1) => resolveShimPath(match$1[1], baseDir));
4067
+ }
4068
+ async function resolveNpmCmdShim(command) {
4069
+ const lower = command.toLowerCase();
4070
+ if (!SHELL_EXTENSIONS.some((ext) => lower.endsWith(ext))) return void 0;
4071
+ let text$1;
4072
+ try {
4073
+ text$1 = await readFile(command, "utf8");
4074
+ } catch {
4075
+ return void 0;
4076
+ }
4077
+ const baseDir = dirname(command);
4078
+ const targets = quotedDp0Targets(text$1, baseDir);
4079
+ const nativeTarget = targets.find((target) => /\.(exe|com)$/i.test(target) && !/[\\/]node\.exe$/i.test(target));
4080
+ if (nativeTarget && await exists$1(nativeTarget)) return asResolved(nativeTarget, false);
4081
+ const scriptTarget = targets.find((target) => /\.(cjs|js|mjs)$/i.test(target));
4082
+ if (scriptTarget && await exists$1(scriptTarget)) {
4083
+ const localNode = join(baseDir, "node.exe");
4084
+ const node = await exists$1(localNode) ? localNode : process$1.execPath;
4085
+ return asResolved(node, false, [scriptTarget]);
4086
+ }
4087
+ return void 0;
4088
+ }
4089
+ async function resolveLaunchBin(command, opts = {}) {
4090
+ const platform = opts.platform ?? process$1.platform;
4091
+ if (platform !== "win32") return asResolved(command, false);
4092
+ const env = opts.env ?? process$1.env;
4093
+ const candidates = windowsCandidates(command, env);
4094
+ const resolved = await firstExisting(candidates);
4095
+ if (!resolved) return asResolved(command, false);
4096
+ const lower = resolved.toLowerCase();
4097
+ const shimTarget = await resolveNpmCmdShim(resolved);
4098
+ if (shimTarget) return shimTarget;
4099
+ return asResolved(resolved, SHELL_EXTENSIONS.some((ext) => lower.endsWith(ext)));
4100
+ }
4101
+
3696
4102
  //#endregion
3697
4103
  //#region src/cli/rewrite/jsonc.ts
3698
4104
  /**
@@ -3771,10 +4177,10 @@ async function readAgentEnv(settingsPath) {
3771
4177
  const CLAUDE_CODE = {
3772
4178
  agent: "claude-code",
3773
4179
  bins: ["claude", "claude-code"],
3774
- async build(baseUrl, extraEnv) {
4180
+ async build(baseUrl, opts) {
3775
4181
  const env = await readAgentEnv(paths.agent.claudeCode.settings);
3776
4182
  env.ANTHROPIC_BASE_URL = baseUrl;
3777
- for (const [k, v] of Object.entries(extraEnv ?? {})) if (env[k] === void 0 && process$1.env[k] === void 0) env[k] = v;
4183
+ for (const [k, v] of Object.entries(opts?.extraEnv ?? {})) if (env[k] === void 0 && process$1.env[k] === void 0) env[k] = v;
3778
4184
  return {
3779
4185
  argvPrefix: ["--settings", JSON.stringify({ env })],
3780
4186
  env: {}
@@ -3784,7 +4190,8 @@ const CLAUDE_CODE = {
3784
4190
  const CODEX = {
3785
4191
  agent: "codex",
3786
4192
  bins: ["codex"],
3787
- async build(baseUrl) {
4193
+ async build(baseUrl, opts) {
4194
+ const wireApi = opts?.wireApi ?? "responses";
3788
4195
  const argvPrefix = [
3789
4196
  "-c",
3790
4197
  "model_provider=afw",
@@ -3793,7 +4200,7 @@ const CODEX = {
3793
4200
  "-c",
3794
4201
  "model_providers.afw.name=\"afw (OpenAI)\"",
3795
4202
  "-c",
3796
- "model_providers.afw.wire_api=\"responses\"",
4203
+ `model_providers.afw.wire_api="${wireApi}"`,
3797
4204
  "-c",
3798
4205
  "model_providers.afw.requires_openai_auth=true"
3799
4206
  ];
@@ -3820,7 +4227,7 @@ function wiringForBin(bin, override) {
3820
4227
  //#region src/cli/launch/instance.ts
3821
4228
  /** Slug a label into a URL/policy-safe instance id; fall back to the pid. */
3822
4229
  function instanceIdFrom(label) {
3823
- if (label && label.trim()) {
4230
+ if (label?.trim()) {
3824
4231
  const slug = label.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
3825
4232
  if (slug) return slug;
3826
4233
  }
@@ -3876,18 +4283,28 @@ async function launchInstance(opts) {
3876
4283
  } : null;
3877
4284
  if (target !== null) await setInstancePolicy(routeKey, target);
3878
4285
  const extraEnv = wiring.agent === "claude-code" && !opts.monitor && opts.model ? await resolveAutoCompactEnv(wiring.agent, opts.model) : {};
3879
- const plan = await wiring.build(baseUrl, extraEnv);
4286
+ const wireApi = wiring.agent === "codex" && !opts.monitor ? (await resolveCodexWireProtocol(opts.model)).wireApi : void 0;
4287
+ const plan = await wiring.build(baseUrl, {
4288
+ extraEnv,
4289
+ ...wireApi ? { wireApi } : {}
4290
+ });
3880
4291
  argvPrefix = plan.argvPrefix;
3881
4292
  envOverride = plan.env;
3882
4293
  }
3883
4294
  const mode = opts.raw ? "raw (bypassing afw)" : opts.monitor ? "monitor-only" : opts.model ? `→ ${opts.model}` : "type default";
3884
4295
  logger.print(`▶ ${wiring.agent}@${instanceId} ${mode}`);
3885
- const child = spawn(opts.bin, [...argvPrefix, ...opts.args], {
4296
+ const resolvedBin = await resolveLaunchBin(opts.bin);
4297
+ const child = spawn(resolvedBin.command, [
4298
+ ...resolvedBin.argsPrefix,
4299
+ ...argvPrefix,
4300
+ ...opts.args
4301
+ ], {
3886
4302
  stdio: "inherit",
3887
4303
  env: {
3888
4304
  ...process$1.env,
3889
4305
  ...envOverride
3890
- }
4306
+ },
4307
+ shell: resolvedBin.shell
3891
4308
  });
3892
4309
  const cleanup = async () => {
3893
4310
  if (opts.ephemeral && !opts.raw) try {
@@ -3905,6 +4322,553 @@ async function launchInstance(opts) {
3905
4322
  });
3906
4323
  }
3907
4324
 
4325
+ //#endregion
4326
+ //#region src/core/provider-catalog.ts
4327
+ const PROVIDER_CATALOG = [
4328
+ {
4329
+ id: "openai",
4330
+ label: "OpenAI",
4331
+ baseUrl: "https://api.openai.com/v1",
4332
+ api: "openai-responses",
4333
+ apiKeyUrl: "https://platform.openai.com/api-keys",
4334
+ oauthKey: "openai",
4335
+ models: [
4336
+ {
4337
+ id: "gpt-5.5",
4338
+ label: "GPT-5.5",
4339
+ vision: true,
4340
+ contextWindow: 1e6,
4341
+ maxTokens: 128e3
4342
+ },
4343
+ {
4344
+ id: "gpt-5.4",
4345
+ label: "GPT-5.4",
4346
+ vision: true,
4347
+ contextWindow: 272e3,
4348
+ maxTokens: 128e3
4349
+ },
4350
+ {
4351
+ id: "gpt-5.4-mini",
4352
+ label: "GPT-5.4 mini",
4353
+ vision: true,
4354
+ contextWindow: 4e5,
4355
+ maxTokens: 128e3
4356
+ },
4357
+ {
4358
+ id: "gpt-5.3-codex",
4359
+ label: "GPT-5.3 Codex",
4360
+ vision: true,
4361
+ contextWindow: 4e5,
4362
+ maxTokens: 128e3
4363
+ },
4364
+ {
4365
+ id: "o3",
4366
+ label: "o3",
4367
+ vision: true,
4368
+ contextWindow: 2e5,
4369
+ maxTokens: 1e5
4370
+ },
4371
+ {
4372
+ id: "o4-mini",
4373
+ label: "o4-mini",
4374
+ vision: true,
4375
+ contextWindow: 2e5,
4376
+ maxTokens: 1e5
4377
+ }
4378
+ ]
4379
+ },
4380
+ {
4381
+ id: "anthropic",
4382
+ label: "Anthropic",
4383
+ baseUrl: "https://api.anthropic.com",
4384
+ api: "anthropic",
4385
+ apiKeyUrl: "https://console.anthropic.com/settings/keys",
4386
+ oauthKey: "anthropic",
4387
+ models: [
4388
+ {
4389
+ id: "claude-opus-4-8",
4390
+ label: "Claude Opus 4.8",
4391
+ vision: true,
4392
+ contextWindow: 1048576,
4393
+ maxTokens: 128e3
4394
+ },
4395
+ {
4396
+ id: "claude-opus-4-7",
4397
+ label: "Claude Opus 4.7",
4398
+ vision: true,
4399
+ contextWindow: 2e5,
4400
+ maxTokens: 64e3
4401
+ },
4402
+ {
4403
+ id: "claude-sonnet-4-6",
4404
+ label: "Claude Sonnet 4.6",
4405
+ vision: true,
4406
+ contextWindow: 2e5,
4407
+ maxTokens: 64e3
4408
+ }
4409
+ ]
4410
+ },
4411
+ {
4412
+ id: "deepseek",
4413
+ label: "DeepSeek",
4414
+ baseUrl: "https://api.deepseek.com",
4415
+ api: "openai-chat",
4416
+ apiKeyUrl: "https://platform.deepseek.com/api_keys",
4417
+ models: [
4418
+ {
4419
+ id: "deepseek-v4-pro",
4420
+ label: "DeepSeek V4 Pro",
4421
+ contextWindow: 1e6,
4422
+ maxTokens: 384e3
4423
+ },
4424
+ {
4425
+ id: "deepseek-v4-flash",
4426
+ label: "DeepSeek V4 Flash",
4427
+ contextWindow: 1e6,
4428
+ maxTokens: 384e3
4429
+ },
4430
+ {
4431
+ id: "deepseek-chat",
4432
+ label: "DeepSeek Chat",
4433
+ contextWindow: 131072,
4434
+ maxTokens: 8192
4435
+ },
4436
+ {
4437
+ id: "deepseek-reasoner",
4438
+ label: "DeepSeek Reasoner",
4439
+ contextWindow: 131072,
4440
+ maxTokens: 65536
4441
+ }
4442
+ ]
4443
+ },
4444
+ {
4445
+ id: "zai",
4446
+ label: "Z.AI (GLM)",
4447
+ baseUrl: "https://api.z.ai/api/paas/v4",
4448
+ api: "openai-chat",
4449
+ apiKeyUrl: "https://z.ai/manage-apikey/apikey-list",
4450
+ models: [
4451
+ {
4452
+ id: "glm-5.1",
4453
+ label: "GLM-5.1",
4454
+ contextWindow: 202800,
4455
+ maxTokens: 131100
4456
+ },
4457
+ {
4458
+ id: "glm-5",
4459
+ label: "GLM-5",
4460
+ contextWindow: 202800,
4461
+ maxTokens: 131100
4462
+ },
4463
+ {
4464
+ id: "glm-5v-turbo",
4465
+ label: "GLM-5V Turbo",
4466
+ vision: true,
4467
+ contextWindow: 202800,
4468
+ maxTokens: 131100
4469
+ },
4470
+ {
4471
+ id: "glm-4.7",
4472
+ label: "GLM-4.7",
4473
+ contextWindow: 204800,
4474
+ maxTokens: 131072
4475
+ }
4476
+ ]
4477
+ },
4478
+ {
4479
+ id: "moonshot",
4480
+ label: "Moonshot (Kimi)",
4481
+ baseUrl: "https://api.moonshot.ai/v1",
4482
+ api: "openai-chat",
4483
+ apiKeyUrl: "https://platform.moonshot.ai/console/api-keys",
4484
+ models: [
4485
+ {
4486
+ id: "kimi-k2.6",
4487
+ label: "Kimi K2.6",
4488
+ vision: true,
4489
+ contextWindow: 262144,
4490
+ maxTokens: 262144
4491
+ },
4492
+ {
4493
+ id: "kimi-k2.5",
4494
+ label: "Kimi K2.5",
4495
+ vision: true,
4496
+ contextWindow: 262144,
4497
+ maxTokens: 262144
4498
+ },
4499
+ {
4500
+ id: "kimi-k2-thinking",
4501
+ label: "Kimi K2 Thinking",
4502
+ contextWindow: 262144,
4503
+ maxTokens: 262144
4504
+ }
4505
+ ]
4506
+ },
4507
+ {
4508
+ id: "mistral",
4509
+ label: "Mistral",
4510
+ baseUrl: "https://api.mistral.ai/v1",
4511
+ api: "openai-chat",
4512
+ apiKeyUrl: "https://console.mistral.ai/api-keys",
4513
+ models: [
4514
+ {
4515
+ id: "mistral-large-latest",
4516
+ label: "Mistral Large",
4517
+ vision: true,
4518
+ contextWindow: 262144,
4519
+ maxTokens: 16384
4520
+ },
4521
+ {
4522
+ id: "mistral-medium-3-5",
4523
+ label: "Mistral Medium 3.5",
4524
+ vision: true,
4525
+ contextWindow: 262144,
4526
+ maxTokens: 8192
4527
+ },
4528
+ {
4529
+ id: "codestral-latest",
4530
+ label: "Codestral",
4531
+ contextWindow: 256e3,
4532
+ maxTokens: 4096
4533
+ },
4534
+ {
4535
+ id: "devstral-medium-latest",
4536
+ label: "Devstral 2",
4537
+ contextWindow: 262144,
4538
+ maxTokens: 32768
4539
+ }
4540
+ ]
4541
+ },
4542
+ {
4543
+ id: "groq",
4544
+ label: "Groq",
4545
+ baseUrl: "https://api.groq.com/openai/v1",
4546
+ api: "openai-chat",
4547
+ apiKeyUrl: "https://console.groq.com/keys",
4548
+ models: [
4549
+ {
4550
+ id: "openai/gpt-oss-120b",
4551
+ label: "GPT OSS 120B",
4552
+ contextWindow: 131072,
4553
+ maxTokens: 65536
4554
+ },
4555
+ {
4556
+ id: "qwen/qwen3-32b",
4557
+ label: "Qwen3 32B",
4558
+ contextWindow: 131072,
4559
+ maxTokens: 40960
4560
+ },
4561
+ {
4562
+ id: "llama-3.3-70b-versatile",
4563
+ label: "Llama 3.3 70B Versatile",
4564
+ contextWindow: 131072,
4565
+ maxTokens: 32768
4566
+ }
4567
+ ]
4568
+ },
4569
+ {
4570
+ id: "openrouter",
4571
+ label: "OpenRouter",
4572
+ baseUrl: "https://openrouter.ai/api/v1",
4573
+ api: "openai-chat",
4574
+ apiKeyUrl: "https://openrouter.ai/keys",
4575
+ models: []
4576
+ },
4577
+ {
4578
+ id: "together",
4579
+ label: "Together AI",
4580
+ baseUrl: "https://api.together.xyz/v1",
4581
+ api: "openai-chat",
4582
+ apiKeyUrl: "https://api.together.ai/settings/api-keys",
4583
+ models: [
4584
+ {
4585
+ id: "deepseek-ai/DeepSeek-V4-Pro",
4586
+ label: "DeepSeek V4 Pro",
4587
+ contextWindow: 512e3,
4588
+ maxTokens: 8192
4589
+ },
4590
+ {
4591
+ id: "zai-org/GLM-5.1",
4592
+ label: "GLM 5.1",
4593
+ contextWindow: 202752,
4594
+ maxTokens: 8192
4595
+ },
4596
+ {
4597
+ id: "moonshotai/Kimi-K2.6",
4598
+ label: "Kimi K2.6",
4599
+ vision: true,
4600
+ contextWindow: 262144,
4601
+ maxTokens: 32768
4602
+ }
4603
+ ]
4604
+ },
4605
+ {
4606
+ id: "fireworks",
4607
+ label: "Fireworks AI",
4608
+ baseUrl: "https://api.fireworks.ai/inference/v1",
4609
+ api: "openai-chat",
4610
+ apiKeyUrl: "https://app.fireworks.ai/settings/users/api-keys",
4611
+ models: [{
4612
+ id: "accounts/fireworks/models/kimi-k2p6",
4613
+ label: "Kimi K2.6",
4614
+ vision: true,
4615
+ contextWindow: 262144,
4616
+ maxTokens: 262144
4617
+ }]
4618
+ },
4619
+ {
4620
+ id: "deepinfra",
4621
+ label: "DeepInfra",
4622
+ baseUrl: "https://api.deepinfra.com/v1/openai",
4623
+ api: "openai-chat",
4624
+ apiKeyUrl: "https://deepinfra.com/dash/api_keys",
4625
+ models: [
4626
+ {
4627
+ id: "deepseek-ai/DeepSeek-V4-Flash",
4628
+ label: "DeepSeek V4 Flash",
4629
+ contextWindow: 1048576,
4630
+ maxTokens: 1048576
4631
+ },
4632
+ {
4633
+ id: "zai-org/GLM-5.1",
4634
+ label: "GLM-5.1",
4635
+ contextWindow: 202752,
4636
+ maxTokens: 202752
4637
+ },
4638
+ {
4639
+ id: "moonshotai/Kimi-K2.5",
4640
+ label: "Kimi K2.5",
4641
+ vision: true,
4642
+ contextWindow: 262144,
4643
+ maxTokens: 262144
4644
+ }
4645
+ ]
4646
+ },
4647
+ {
4648
+ id: "novita",
4649
+ label: "Novita AI",
4650
+ baseUrl: "https://api.novita.ai/openai/v1",
4651
+ api: "openai-chat",
4652
+ apiKeyUrl: "https://novita.ai/settings/key-management",
4653
+ models: [{
4654
+ id: "moonshotai/kimi-k2.5",
4655
+ label: "Kimi K2.5",
4656
+ vision: true,
4657
+ contextWindow: 262144,
4658
+ maxTokens: 65536
4659
+ }, {
4660
+ id: "zai-org/glm-5",
4661
+ label: "GLM-5",
4662
+ contextWindow: 202752,
4663
+ maxTokens: 65536
4664
+ }]
4665
+ },
4666
+ {
4667
+ id: "cerebras",
4668
+ label: "Cerebras",
4669
+ baseUrl: "https://api.cerebras.ai/v1",
4670
+ api: "openai-chat",
4671
+ apiKeyUrl: "https://cloud.cerebras.ai/platform",
4672
+ models: [
4673
+ {
4674
+ id: "zai-glm-4.7",
4675
+ label: "Z.ai GLM 4.7",
4676
+ contextWindow: 128e3,
4677
+ maxTokens: 8192
4678
+ },
4679
+ {
4680
+ id: "gpt-oss-120b",
4681
+ label: "GPT OSS 120B",
4682
+ contextWindow: 128e3,
4683
+ maxTokens: 8192
4684
+ },
4685
+ {
4686
+ id: "qwen-3-235b-a22b-instruct-2507",
4687
+ label: "Qwen 3 235B Instruct",
4688
+ contextWindow: 128e3,
4689
+ maxTokens: 8192
4690
+ }
4691
+ ]
4692
+ },
4693
+ {
4694
+ id: "nvidia",
4695
+ label: "NVIDIA",
4696
+ baseUrl: "https://integrate.api.nvidia.com/v1",
4697
+ api: "openai-chat",
4698
+ apiKeyUrl: "https://build.nvidia.com",
4699
+ models: [{
4700
+ id: "nvidia/nemotron-3-super-120b-a12b",
4701
+ label: "Nemotron 3 Super 120B",
4702
+ contextWindow: 262144,
4703
+ maxTokens: 8192
4704
+ }, {
4705
+ id: "z-ai/glm-5.1",
4706
+ label: "GLM 5.1",
4707
+ contextWindow: 202752,
4708
+ maxTokens: 8192
4709
+ }]
4710
+ },
4711
+ {
4712
+ id: "xiaomi",
4713
+ label: "Xiaomi MiMo",
4714
+ baseUrl: "https://api.xiaomimimo.com/v1",
4715
+ api: "openai-chat",
4716
+ models: [{
4717
+ id: "mimo-v2-pro",
4718
+ label: "MiMo V2 Pro",
4719
+ contextWindow: 1048576,
4720
+ maxTokens: 32e3
4721
+ }, {
4722
+ id: "mimo-v2-omni",
4723
+ label: "MiMo V2 Omni",
4724
+ vision: true,
4725
+ contextWindow: 262144,
4726
+ maxTokens: 32e3
4727
+ }]
4728
+ },
4729
+ {
4730
+ id: "stepfun",
4731
+ label: "StepFun",
4732
+ baseUrl: "https://api.stepfun.ai/v1",
4733
+ api: "openai-chat",
4734
+ models: [{
4735
+ id: "step-3.5-flash",
4736
+ label: "Step 3.5 Flash",
4737
+ contextWindow: 262144,
4738
+ maxTokens: 65536
4739
+ }]
4740
+ },
4741
+ {
4742
+ id: "venice",
4743
+ label: "Venice AI",
4744
+ baseUrl: "https://api.venice.ai/api/v1",
4745
+ api: "openai-chat",
4746
+ apiKeyUrl: "https://venice.ai/settings/api",
4747
+ models: [
4748
+ {
4749
+ id: "zai-org-glm-5",
4750
+ label: "GLM 5",
4751
+ contextWindow: 198e3,
4752
+ maxTokens: 32e3
4753
+ },
4754
+ {
4755
+ id: "qwen3-235b-a22b-thinking-2507",
4756
+ label: "Qwen3 235B Thinking",
4757
+ contextWindow: 128e3,
4758
+ maxTokens: 16384
4759
+ },
4760
+ {
4761
+ id: "llama-3.3-70b",
4762
+ label: "Llama 3.3 70B",
4763
+ contextWindow: 128e3,
4764
+ maxTokens: 4096
4765
+ }
4766
+ ]
4767
+ }
4768
+ ];
4769
+
4770
+ //#endregion
4771
+ //#region src/daemon/orchestrator/oauth/jwt.ts
4772
+ /** Decode a JWT's payload claims without verifying the signature, or undefined
4773
+ * when the token is malformed. The upstream still verifies for real. */
4774
+ function decodeJwtPayload(token) {
4775
+ const parts = token.split(".");
4776
+ const payload = parts[1];
4777
+ if (parts.length < 2 || !payload) return void 0;
4778
+ try {
4779
+ const json = Buffer$1.from(payload, "base64url").toString("utf8");
4780
+ const claims = JSON.parse(json);
4781
+ return typeof claims === "object" && claims !== null ? claims : void 0;
4782
+ } catch {
4783
+ return void 0;
4784
+ }
4785
+ }
4786
+ /** The `exp` claim (epoch seconds) of a JWT, or undefined when the token is
4787
+ * malformed or carries no numeric `exp`. */
4788
+ function decodeJwtExp(token) {
4789
+ const exp = decodeJwtPayload(token)?.exp;
4790
+ return typeof exp === "number" ? exp : void 0;
4791
+ }
4792
+
4793
+ //#endregion
4794
+ //#region src/cli/util/select.ts
4795
+ const ESC = "\x1B";
4796
+ /** Drive an interactive selection over `labels`. Returns the chosen 0-based
4797
+ * indices (length 1 in single mode), or null when an interactive UI can't be
4798
+ * shown. Ctrl-C exits the process (130), matching the rest of the CLI prompts. */
4799
+ async function interactiveSelect(question, labels, opts = {}) {
4800
+ const stdin = process$1.stdin;
4801
+ const stdout = process$1.stdout;
4802
+ if (!stdin.isTTY || typeof stdin.setRawMode !== "function" || labels.length === 0) return null;
4803
+ const multi = opts.multi === true;
4804
+ let cursor = 0;
4805
+ const selected = new Set(opts.preselected ?? []);
4806
+ const hint = multi ? "↑/↓ move · space toggle · a all · enter confirm" : "↑/↓ move · enter select";
4807
+ const draw = (first) => {
4808
+ const lines = labels.map((label, i) => {
4809
+ const pointer = i === cursor ? "❯" : " ";
4810
+ const box = multi ? selected.has(i) ? "◉ " : "◯ " : "";
4811
+ const text$1 = `${pointer} ${box}${label}`;
4812
+ return i === cursor ? `${ESC}[36m${text$1}${ESC}[0m` : text$1;
4813
+ });
4814
+ if (!first) stdout.write(`${ESC}[${labels.length}A`);
4815
+ stdout.write(`${ESC}[J`);
4816
+ stdout.write(`${lines.join("\r\n")}\r\n`);
4817
+ };
4818
+ stdout.write(`${question} ${ESC}[2m${hint}${ESC}[0m\r\n`);
4819
+ stdout.write(`${ESC}[?25l`);
4820
+ draw(true);
4821
+ return new Promise((resolve) => {
4822
+ const cleanup = () => {
4823
+ stdin.setRawMode(false);
4824
+ stdin.pause();
4825
+ stdin.removeListener("data", onData);
4826
+ stdout.write(`${ESC}[?25h`);
4827
+ };
4828
+ const finish = (result) => {
4829
+ cleanup();
4830
+ stdout.write("\r\n");
4831
+ resolve(result);
4832
+ };
4833
+ const onData = (chunk) => {
4834
+ if (chunk === "") {
4835
+ cleanup();
4836
+ stdout.write("\r\n");
4837
+ process$1.exit(130);
4838
+ }
4839
+ if (chunk === "\r" || chunk === "\n") {
4840
+ finish(multi ? [...selected].sort((a, b) => a - b) : [cursor]);
4841
+ return;
4842
+ }
4843
+ if (chunk === `${ESC}[A` || chunk === "k") {
4844
+ cursor = (cursor - 1 + labels.length) % labels.length;
4845
+ draw(false);
4846
+ return;
4847
+ }
4848
+ if (chunk === `${ESC}[B` || chunk === "j") {
4849
+ cursor = (cursor + 1) % labels.length;
4850
+ draw(false);
4851
+ return;
4852
+ }
4853
+ if (multi && chunk === " ") {
4854
+ if (selected.has(cursor)) selected.delete(cursor);
4855
+ else selected.add(cursor);
4856
+ draw(false);
4857
+ return;
4858
+ }
4859
+ if (multi && (chunk === "a" || chunk === "A")) {
4860
+ if (selected.size === labels.length) selected.clear();
4861
+ else for (let i = 0; i < labels.length; i++) selected.add(i);
4862
+ draw(false);
4863
+ }
4864
+ };
4865
+ stdin.setRawMode(true);
4866
+ stdin.resume();
4867
+ stdin.setEncoding("utf8");
4868
+ stdin.on("data", onData);
4869
+ });
4870
+ }
4871
+
3908
4872
  //#endregion
3909
4873
  //#region src/cli/util/prompt.ts
3910
4874
  /**
@@ -3951,6 +4915,8 @@ async function promptText(question, def = "") {
3951
4915
  async function promptChoice(question, choices) {
3952
4916
  const first = choices[0];
3953
4917
  if (!process$1.stdin.isTTY) return first;
4918
+ const picked = await interactiveSelect(question, choices, { multi: false });
4919
+ if (picked) return choices[picked[0]];
3954
4920
  const rl = createInterface({
3955
4921
  input: process$1.stdin,
3956
4922
  output: process$1.stdout
@@ -3970,6 +4936,54 @@ async function promptChoice(question, choices) {
3970
4936
  }
3971
4937
  }
3972
4938
  /**
4939
+ * Ask the user to pick zero or more of `choices`. Accepts a comma/space list of
4940
+ * 1-based indices and ranges (`1,3` / `1 3` / `2-5`), or the word `all`. Empty
4941
+ * input selects nothing. Non-interactive (no TTY) returns `[]` so scripted runs
4942
+ * never block. Returns the chosen values in `choices` order, de-duplicated.
4943
+ */
4944
+ async function promptMultiChoice(question, choices) {
4945
+ if (!process$1.stdin.isTTY || choices.length === 0) return [];
4946
+ const picked = await interactiveSelect(question, choices, { multi: true });
4947
+ if (picked) return choices.filter((_, i) => picked.includes(i));
4948
+ const rl = createInterface({
4949
+ input: process$1.stdin,
4950
+ output: process$1.stdout
4951
+ });
4952
+ try {
4953
+ for (;;) {
4954
+ const list = choices.map((c, i) => ` ${i + 1}) ${c}`).join("\n");
4955
+ const answer = (await rl.question(`${question}\n${list}\n(e.g. 1,3 or 2-4 or 'all'; blank for none) `)).trim();
4956
+ if (answer === "") return [];
4957
+ if (answer.toLowerCase() === "all") return [...choices];
4958
+ const picked$1 = new Set();
4959
+ let bad = false;
4960
+ for (const tok of answer.split(/[\s,]+/).filter(Boolean)) {
4961
+ const range = tok.match(/^(\d+)-(\d+)$/);
4962
+ if (range) {
4963
+ const lo = Number.parseInt(range[1], 10);
4964
+ const hi = Number.parseInt(range[2], 10);
4965
+ if (lo < 1 || hi > choices.length || lo > hi) {
4966
+ bad = true;
4967
+ break;
4968
+ }
4969
+ for (let i = lo; i <= hi; i++) picked$1.add(i - 1);
4970
+ continue;
4971
+ }
4972
+ const n = Number.parseInt(tok, 10);
4973
+ if (!Number.isInteger(n) || n < 1 || n > choices.length) {
4974
+ bad = true;
4975
+ break;
4976
+ }
4977
+ picked$1.add(n - 1);
4978
+ }
4979
+ if (bad) continue;
4980
+ return choices.filter((_, i) => picked$1.has(i));
4981
+ }
4982
+ } finally {
4983
+ rl.close();
4984
+ }
4985
+ }
4986
+ /**
3973
4987
  * Read a secret from the terminal without echoing keystrokes. Falls back to a
3974
4988
  * plain line read when stdin is not a TTY, so a piped value still works.
3975
4989
  */
@@ -4013,13 +5027,286 @@ async function promptSecret(question) {
4013
5027
  });
4014
5028
  }
4015
5029
 
5030
+ //#endregion
5031
+ //#region src/cli/oauth/browser.ts
5032
+ function openBrowser$1(url) {
5033
+ const platform = process$1.platform;
5034
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
5035
+ const args = platform === "win32" ? [
5036
+ "/c",
5037
+ "start",
5038
+ "",
5039
+ url
5040
+ ] : [url];
5041
+ try {
5042
+ const child = spawn(cmd, args, {
5043
+ stdio: "ignore",
5044
+ detached: true
5045
+ });
5046
+ child.on("error", () => {});
5047
+ child.unref();
5048
+ } catch {}
5049
+ }
5050
+
5051
+ //#endregion
5052
+ //#region src/cli/oauth/pkce.ts
5053
+ function base64url(buf) {
5054
+ return buf.toString("base64url");
5055
+ }
5056
+ /** A fresh PKCE verifier + S256 challenge pair. */
5057
+ function generatePkce() {
5058
+ const verifier = base64url(randomBytes(32));
5059
+ const challenge = base64url(createHash("sha256").update(verifier).digest());
5060
+ return {
5061
+ verifier,
5062
+ challenge
5063
+ };
5064
+ }
5065
+ /** An opaque anti-CSRF `state` value for the authorize request. */
5066
+ function generateState() {
5067
+ return base64url(randomBytes(16));
5068
+ }
5069
+
5070
+ //#endregion
5071
+ //#region src/cli/oauth/login.ts
5072
+ async function writeJson(path$1, value) {
5073
+ await mkdir(dirname(path$1), { recursive: true });
5074
+ await atomicWrite(path$1, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
5075
+ }
5076
+ /** The ChatGPT account id is carried as a custom claim in the access (or id)
5077
+ * token JWT. The Codex backend rejects requests without it. */
5078
+ function chatgptAccountId(tok) {
5079
+ for (const jwt of [tok.access_token, tok.id_token]) {
5080
+ if (!jwt) continue;
5081
+ const claims = decodeJwtPayload(jwt);
5082
+ const direct = claims?.["https://api.openai.com/auth.chatgpt_account_id"];
5083
+ if (typeof direct === "string" && direct) return direct;
5084
+ const fallback = claims?.["https://api.openai.com/auth.chatgpt_account_user_id"];
5085
+ if (typeof fallback === "string" && fallback) return fallback;
5086
+ }
5087
+ return void 0;
5088
+ }
5089
+ const OAUTH_PROVIDERS = {
5090
+ anthropic: {
5091
+ key: "anthropic",
5092
+ label: "Anthropic (Claude Pro/Max subscription)",
5093
+ clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
5094
+ authorizeUrl: "https://claude.ai/oauth/authorize",
5095
+ tokenUrl: "https://platform.claude.com/v1/oauth/token",
5096
+ scope: "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload",
5097
+ redirectHost: "127.0.0.1",
5098
+ redirectPort: 53692,
5099
+ redirectPath: "/callback",
5100
+ tokenStyle: "json",
5101
+ extraAuthorize: { code: "true" },
5102
+ register: {
5103
+ agent: "claude-code",
5104
+ baseUrl: "https://api.anthropic.com",
5105
+ api: "anthropic-messages"
5106
+ },
5107
+ async persist(tok) {
5108
+ const expiresAt = Date.now() + (tok.expires_in ?? 3600) * 1e3;
5109
+ await writeJson(paths.oauth.claudeCode, { claudeAiOauth: {
5110
+ accessToken: tok.access_token,
5111
+ refreshToken: tok.refresh_token,
5112
+ expiresAt
5113
+ } });
5114
+ }
5115
+ },
5116
+ openai: {
5117
+ key: "openai",
5118
+ label: "OpenAI (ChatGPT/Codex subscription)",
5119
+ clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
5120
+ authorizeUrl: "https://auth.openai.com/oauth/authorize",
5121
+ tokenUrl: "https://auth.openai.com/oauth/token",
5122
+ scope: "openid profile email offline_access",
5123
+ redirectHost: "localhost",
5124
+ redirectPort: 1455,
5125
+ redirectPath: "/auth/callback",
5126
+ tokenStyle: "form",
5127
+ extraAuthorize: {
5128
+ id_token_add_organizations: "true",
5129
+ codex_cli_simplified_flow: "true",
5130
+ originator: "afw"
5131
+ },
5132
+ register: {
5133
+ agent: "codex",
5134
+ baseUrl: "https://chatgpt.com/backend-api/codex",
5135
+ api: "openai-responses"
5136
+ },
5137
+ async persist(tok) {
5138
+ await writeJson(paths.oauth.codex, {
5139
+ auth_mode: "chatgpt",
5140
+ tokens: {
5141
+ access_token: tok.access_token,
5142
+ refresh_token: tok.refresh_token,
5143
+ ...tok.id_token ? { id_token: tok.id_token } : {},
5144
+ ...chatgptAccountId(tok) ? { account_id: chatgptAccountId(tok) } : {}
5145
+ },
5146
+ last_refresh: new Date().toISOString()
5147
+ });
5148
+ }
5149
+ }
5150
+ };
5151
+ function redirectUri(def) {
5152
+ return `http://${def.redirectHost}:${def.redirectPort}${def.redirectPath}`;
5153
+ }
5154
+ function authorizeUrl(def, challenge, state$1) {
5155
+ const u = new URL(def.authorizeUrl);
5156
+ const params = {
5157
+ response_type: "code",
5158
+ client_id: def.clientId,
5159
+ redirect_uri: redirectUri(def),
5160
+ scope: def.scope,
5161
+ code_challenge: challenge,
5162
+ code_challenge_method: "S256",
5163
+ state: state$1,
5164
+ ...def.extraAuthorize
5165
+ };
5166
+ for (const [k, v] of Object.entries(params)) u.searchParams.set(k, v);
5167
+ return u.toString();
5168
+ }
5169
+ /** Pull the authorization code out of whatever the user pasted: a bare code, a
5170
+ * `code#state` pair, or the full redirected URL. Returns undefined when it
5171
+ * can't find one or the state doesn't match. */
5172
+ function parsePastedCode(input, state$1) {
5173
+ const text$1 = input.trim();
5174
+ if (text$1 === "") return void 0;
5175
+ if (text$1.includes("://")) {
5176
+ try {
5177
+ const u = new URL(text$1);
5178
+ const code$1 = u.searchParams.get("code");
5179
+ const got$1 = u.searchParams.get("state");
5180
+ if (code$1 && (!got$1 || got$1 === state$1)) return code$1;
5181
+ } catch {
5182
+ return void 0;
5183
+ }
5184
+ return void 0;
5185
+ }
5186
+ const [code, got] = text$1.split("#");
5187
+ if (code && (!got || got === state$1)) return code;
5188
+ return void 0;
5189
+ }
5190
+ const OK_PAGE = "<!doctype html><meta charset=\"utf-8\"><title>afw</title><body style=\"font-family:system-ui;padding:3rem;text-align:center\"><h2>✓ afw is now connected</h2><p>You can close this tab and return to the terminal.</p>";
5191
+ const ERR_PAGE = "<!doctype html><meta charset=\"utf-8\"><title>afw</title><body style=\"font-family:system-ui;padding:3rem;text-align:center\"><h2>Login failed</h2><p>Return to the terminal and try again.</p>";
5192
+ /** Wait for the authorization code via either the localhost callback or a
5193
+ * hand-pasted code — whichever arrives first. */
5194
+ function awaitAuthCode(def, state$1) {
5195
+ return new Promise((resolve, reject) => {
5196
+ let settled = false;
5197
+ const server = createServer((req, res) => {
5198
+ const url = new URL(req.url ?? "/", `http://${def.redirectHost}:${def.redirectPort}`);
5199
+ if (url.pathname !== def.redirectPath) {
5200
+ res.writeHead(404);
5201
+ res.end();
5202
+ return;
5203
+ }
5204
+ const code = url.searchParams.get("code");
5205
+ const got = url.searchParams.get("state");
5206
+ if (!code || got && got !== state$1) {
5207
+ res.writeHead(400, { "content-type": "text/html" });
5208
+ res.end(ERR_PAGE);
5209
+ finish(void 0, new Error("OAuth callback carried no code or a mismatched state"));
5210
+ return;
5211
+ }
5212
+ res.writeHead(200, { "content-type": "text/html" });
5213
+ res.end(OK_PAGE);
5214
+ finish(code);
5215
+ });
5216
+ const finish = (code, err) => {
5217
+ if (settled) return;
5218
+ settled = true;
5219
+ server.close();
5220
+ if (code) resolve(code);
5221
+ else reject(err ?? new Error("OAuth login cancelled"));
5222
+ };
5223
+ server.on("error", (err) => finish(void 0, err));
5224
+ server.listen(def.redirectPort, def.redirectHost);
5225
+ promptText(" …or paste the authorization code / redirect URL here").then((answer) => {
5226
+ if (settled) return;
5227
+ const code = parsePastedCode(answer, state$1);
5228
+ if (code) finish(code);
5229
+ });
5230
+ });
5231
+ }
5232
+ async function exchangeCode(def, code, verifier, state$1) {
5233
+ const fields = {
5234
+ grant_type: "authorization_code",
5235
+ client_id: def.clientId,
5236
+ code,
5237
+ code_verifier: verifier,
5238
+ redirect_uri: redirectUri(def),
5239
+ state: state$1
5240
+ };
5241
+ const res = def.tokenStyle === "json" ? await fetch(def.tokenUrl, {
5242
+ method: "POST",
5243
+ headers: { "content-type": "application/json" },
5244
+ body: JSON.stringify(fields)
5245
+ }) : await fetch(def.tokenUrl, {
5246
+ method: "POST",
5247
+ headers: { "content-type": "application/x-www-form-urlencoded" },
5248
+ body: new URLSearchParams(fields).toString()
5249
+ });
5250
+ if (!res.ok) {
5251
+ const body = (await res.text().catch(() => "")).slice(0, 300);
5252
+ throw new Error(`token exchange failed: HTTP ${res.status} ${body}`);
5253
+ }
5254
+ return await res.json();
5255
+ }
5256
+ /** Run an interactive OAuth login for `key`. Opens the browser, waits for the
5257
+ * callback (or a pasted code), exchanges it, and writes the tokens to afw's
5258
+ * own store. Returns the provider def on success (so the caller can register
5259
+ * the provider), or null when not on a TTY / cancelled. */
5260
+ async function oauthLogin(key) {
5261
+ if (!process$1.stdin.isTTY) return null;
5262
+ const def = OAUTH_PROVIDERS[key];
5263
+ const { verifier, challenge } = generatePkce();
5264
+ const state$1 = generateState();
5265
+ const url = authorizeUrl(def, challenge, state$1);
5266
+ logger.print(`\nLogging in to ${def.label} via afw.`);
5267
+ logger.print("Opening your browser to authorize. If it does not open, visit:\n");
5268
+ logger.print(` ${url}\n`);
5269
+ openBrowser$1(url);
5270
+ let code;
5271
+ try {
5272
+ code = await awaitAuthCode(def, state$1);
5273
+ } catch (err) {
5274
+ logger.print(` ✗ login failed: ${err.message}`);
5275
+ return null;
5276
+ }
5277
+ try {
5278
+ const tokens = await exchangeCode(def, code, verifier, state$1);
5279
+ if (!tokens.access_token || !tokens.refresh_token) throw new Error("token endpoint returned no access/refresh token");
5280
+ await def.persist(tokens);
5281
+ } catch (err) {
5282
+ logger.print(` ✗ ${err.message}`);
5283
+ if (await confirmYesNo(" Try the login again?", false)) return oauthLogin(key);
5284
+ return null;
5285
+ }
5286
+ logger.print(` ✓ logged in — afw stored your ${def.label} token locally.`);
5287
+ return def;
5288
+ }
5289
+
4016
5290
  //#endregion
4017
5291
  //#region src/cli/commands/route.ts
4018
- const MODEL_APIS$2 = [
5292
+ const MODEL_APIS$1 = [
4019
5293
  "anthropic-messages",
4020
5294
  "openai-chat",
4021
5295
  "openai-responses"
4022
5296
  ];
5297
+ function normalizeReasoningEffortFlag(raw$1) {
5298
+ if (raw$1 == null) return void 0;
5299
+ const value = raw$1.trim().toLowerCase();
5300
+ return REASONING_EFFORTS.includes(value) ? value : void 0;
5301
+ }
5302
+ function normalizeGenerationPathFlag(raw$1) {
5303
+ if (raw$1 == null) return void 0;
5304
+ const value = raw$1.trim().toLowerCase();
5305
+ return GENERATION_PATH_MODES.includes(value) ? value : void 0;
5306
+ }
5307
+ function providerSecretRef(id) {
5308
+ return `provider:${id}`;
5309
+ }
4023
5310
  async function apiFetch(method, path$1, body) {
4024
5311
  let res;
4025
5312
  try {
@@ -4141,13 +5428,12 @@ const setCmd$1 = new Command("set").description("Route an <agent>/<sourceModel>
4141
5428
  kind: "composite",
4142
5429
  comboId: opts.fusion
4143
5430
  } : { kind: "passthrough" };
4144
- const res = await apiFetch("POST", "/api/routing/agent", {
5431
+ await apiFetch("POST", "/api/routing/agent", {
4145
5432
  routeKey,
4146
5433
  target
4147
5434
  });
4148
5435
  const combos = target.kind === "composite" ? (await apiFetch("GET", "/api/routing/registry")).combos : void 0;
4149
5436
  logger.print(`✓ ${routeKey} ${describeTarget$2(target, combos)}`);
4150
- if (res.seededRoute) logger.print(` + created wire route ${res.seededRoute} (decoder openai-chat)`);
4151
5437
  }));
4152
5438
  const unsetCmd$1 = new Command("unset").description("Remove a per-source-model routing entry. The source model inherits the agent's <agent>/* default again. Use `route set <key> --passthrough` instead if you want to pin it to passthrough explicitly.").argument("<routeKey>", "route key, e.g. claude-code/claude-opus-4-7").action((routeKey) => run$3(async () => {
4153
5439
  await apiFetch("DELETE", `/api/routing/agent?routeKey=${encodeURIComponent(routeKey)}`);
@@ -4194,18 +5480,26 @@ const fusionList = new Command("list").description("List configured Model Fusion
4194
5480
  }
4195
5481
  }));
4196
5482
  const fusionCmd = new Command("fusion").description("List Model Fusion combos (build them on the dashboard).").addCommand(fusionList);
4197
- const providerAdd = new Command("add").description("Register a model provider.").argument("<id>", "provider id").requiredOption("--base-url <url>", "provider base URL").requiredOption("--api <api>", `wire format: ${MODEL_APIS$2.join(" | ")}`).option("--auth <kind>", "passthrough | bearer | api-key", "passthrough").option("--header <name>", "header name for api-key auth, e.g. x-api-key").option("--label <text>", "display label").option("--key <value>", "API key (prompted without echo if omitted)").action((id, opts) => run$3(async () => {
4198
- if (!MODEL_APIS$2.includes(opts.api)) return fail(`--api must be one of ${MODEL_APIS$2.join(", ")}`);
5483
+ const providerAdd = new Command("add").description("Register a model provider.").argument("<id>", "provider id").requiredOption("--base-url <url>", "provider base URL").requiredOption("--api <api>", `wire format: ${MODEL_APIS$1.join(" | ")}`).option("--auth <kind>", "passthrough | bearer | api-key", "passthrough").option("--header <name>", "header name for api-key auth, e.g. x-api-key").option("--label <text>", "display label").option("--key <value>", "API key (prompted without echo if omitted)").option("--generation-path <mode>", `generation endpoint path mode: ${GENERATION_PATH_MODES.join(" | ")}`).option("--reasoning-effort <effort>", `provider default reasoning effort: ${REASONING_EFFORTS.join(" | ")}`).action((id, opts) => run$3(async () => {
5484
+ if (!MODEL_APIS$1.includes(opts.api)) return fail(`--api must be one of ${MODEL_APIS$1.join(", ")}`);
4199
5485
  if (![
4200
5486
  "passthrough",
4201
5487
  "bearer",
4202
5488
  "api-key"
4203
5489
  ].includes(opts.auth)) return fail("--auth must be passthrough, bearer, or api-key");
4204
5490
  if (opts.auth === "api-key" && !opts.header) return fail("--header is required for api-key auth");
5491
+ const generationPath = normalizeGenerationPathFlag(opts.generationPath);
5492
+ if (opts.generationPath != null && !generationPath) return fail(`--generation-path must be one of ${GENERATION_PATH_MODES.join(", ")}`);
5493
+ const reasoningEffort = normalizeReasoningEffortFlag(opts.reasoningEffort);
5494
+ if (opts.reasoningEffort != null && !reasoningEffort) return fail(`--reasoning-effort must be one of ${REASONING_EFFORTS.join(", ")}`);
4205
5495
  let apiKey = opts.key;
4206
5496
  if ((opts.auth === "bearer" || opts.auth === "api-key") && !apiKey) {
4207
- apiKey = await promptSecret(`API key for ${id}: `);
4208
- if (!apiKey) return fail("an API key is required for this auth kind");
5497
+ const reg = await apiFetch("GET", "/api/routing/registry");
5498
+ const hasExistingSecret = reg.secretRefs.includes(providerSecretRef(id));
5499
+ if (!hasExistingSecret) {
5500
+ apiKey = await promptSecret(`API key for ${id}: `);
5501
+ if (!apiKey) return fail("an API key is required for this auth kind");
5502
+ }
4209
5503
  }
4210
5504
  await apiFetch("POST", "/api/routing/provider", {
4211
5505
  id,
@@ -4214,7 +5508,9 @@ const providerAdd = new Command("add").description("Register a model provider.")
4214
5508
  authKind: opts.auth,
4215
5509
  ...opts.header ? { authHeader: opts.header } : {},
4216
5510
  ...opts.label ? { label: opts.label } : {},
4217
- ...apiKey ? { apiKey } : {}
5511
+ ...apiKey ? { apiKey } : {},
5512
+ ...generationPath ? { generationPath } : {},
5513
+ ...reasoningEffort ? { reasoningEffort } : {}
4218
5514
  });
4219
5515
  logger.print(`✓ provider ${id} registered`);
4220
5516
  }));
@@ -4228,13 +5524,44 @@ const providerList = new Command("list").description("List registered providers.
4228
5524
  logger.print("No providers.");
4229
5525
  return;
4230
5526
  }
4231
- for (const p of reg.providers) logger.print(` ${p.id.padEnd(20)} ${p.api.padEnd(20)} ${p.auth.kind.padEnd(12)} ${p.origin.padEnd(8)} ${p.baseUrl}`);
5527
+ for (const p of reg.providers) {
5528
+ const path$1 = `path=${p.generationPath ?? "versioned"}`;
5529
+ const effort = p.reasoningEffort ? ` effort=${p.reasoningEffort}` : "";
5530
+ logger.print(` ${p.id.padEnd(20)} ${p.api.padEnd(20)} ${path$1.padEnd(14)}${effort.padEnd(14)} ${p.auth.kind.padEnd(12)} ${p.origin.padEnd(8)} ${p.baseUrl}`);
5531
+ }
4232
5532
  }));
4233
5533
  const providerCmd = new Command("provider").description("Manage model providers.").addCommand(providerAdd).addCommand(providerRm).addCommand(providerList);
4234
5534
  const modelRm = new Command("rm").description("Remove a model.").argument("<id>", "model id").action((id) => run$3(async () => {
4235
5535
  await apiFetch("DELETE", `/api/routing/model?id=${encodeURIComponent(id)}`);
4236
5536
  logger.print(`✓ model ${id} removed`);
4237
5537
  }));
5538
+ const modelSet = new Command("set").description("Update model routing metadata.").argument("<id>", "model id").option("--provider <id>", "disambiguate provider id").option("--reasoning-effort <effort>", `model reasoning effort: ${REASONING_EFFORTS.join(" | ")}`).option("--clear-reasoning-effort", "clear the model reasoning effort override").action((id, opts) => run$3(async () => {
5539
+ const wantsEffort = opts.reasoningEffort != null;
5540
+ const wantsClear = opts.clearReasoningEffort === true;
5541
+ if (wantsEffort === wantsClear) return fail("pass exactly one of --reasoning-effort or --clear-reasoning-effort");
5542
+ const effort = normalizeReasoningEffortFlag(opts.reasoningEffort);
5543
+ if (wantsEffort && !effort) return fail(`--reasoning-effort must be one of ${REASONING_EFFORTS.join(", ")}`);
5544
+ const reg = await apiFetch("GET", "/api/routing/registry");
5545
+ const matches = reg.models.filter((m) => m.id === id && (!opts.provider || m.providerId === opts.provider));
5546
+ if (matches.length === 0) {
5547
+ const suffix = opts.provider ? ` under provider "${opts.provider}"` : "";
5548
+ return fail(`unknown model "${id}"${suffix}`);
5549
+ }
5550
+ if (matches.length > 1) return fail(`model "${id}" exists under multiple providers: ${matches.map((m) => m.providerId).join(", ")}; pass --provider`);
5551
+ const model = matches[0];
5552
+ await apiFetch("POST", "/api/routing/model", {
5553
+ id: model.id,
5554
+ providerId: model.providerId,
5555
+ label: model.label,
5556
+ ...model.api ? { api: model.api } : {},
5557
+ input: model.input,
5558
+ ...typeof model.contextWindow === "number" ? { contextWindow: model.contextWindow } : {},
5559
+ ...typeof model.maxTokens === "number" ? { maxTokens: model.maxTokens } : {},
5560
+ ...model.cost ? { cost: model.cost } : {},
5561
+ ...effort ? { reasoningEffort: effort } : {}
5562
+ });
5563
+ logger.print(`✓ model ${model.providerId}/${model.id} reasoning effort → ${effort ?? "provider default"}`);
5564
+ }));
4238
5565
  const secretSet = new Command("set").description("Store a secret value under a ref (prompted, no echo).").argument("<ref>", "secret ref, e.g. provider:my-provider").action((ref) => run$3(async () => {
4239
5566
  const value = await promptSecret(`Value for ${ref}: `);
4240
5567
  if (!value) return fail("no value entered");
@@ -4409,11 +5736,106 @@ function providerIdFromUrl(baseUrl) {
4409
5736
  return "custom";
4410
5737
  }
4411
5738
  }
5739
+ const CUSTOM_PROVIDER_LABEL = "Custom — enter a base URL manually";
4412
5740
  /** Run the interactive provider wizard once. Returns the registered provider
4413
5741
  * id and its model ids on success, or null when the user skipped it. Exported
4414
- * so the first-run onboarding flow can reuse the exact same prompts + probe. */
5742
+ * so the first-run onboarding flow can reuse the exact same prompts + probe.
5743
+ *
5744
+ * Starts from a curated catalog of well-known providers (base URL + wire
5745
+ * format + known models pre-filled) so the common case is a couple of menu
5746
+ * picks; "Custom" drops to the manual base-URL flow for anything not listed. */
4415
5747
  async function addOneProvider() {
4416
- let baseUrl = await promptText("Provider base URL (e.g. https://api.openai.com/v1)");
5748
+ if (!process$1.stdin.isTTY) return addCustomProvider();
5749
+ const labels = [...PROVIDER_CATALOG.map((p) => p.label), CUSTOM_PROVIDER_LABEL];
5750
+ const choice = await promptChoice("Which model provider?", labels);
5751
+ if (choice === CUSTOM_PROVIDER_LABEL) return addCustomProvider();
5752
+ const preset = PROVIDER_CATALOG.find((p) => p.label === choice);
5753
+ return preset ? addCatalogProvider(preset) : addCustomProvider();
5754
+ }
5755
+ /** Catalog path: provider host + wire format are known, so we only ask which
5756
+ * models to enable (multi-select from the known list, plus any extra ids) and
5757
+ * for the API key. */
5758
+ async function addCatalogProvider(preset) {
5759
+ logger.print(`\n${preset.label} — ${preset.baseUrl}`);
5760
+ const models = [];
5761
+ if (preset.models.length > 0) {
5762
+ const labels = preset.models.map((m) => `${m.label} (${m.id})`);
5763
+ const chosen = await promptMultiChoice("Select models to enable", labels);
5764
+ for (const lbl of chosen) {
5765
+ const m = preset.models[labels.indexOf(lbl)];
5766
+ if (m) models.push({
5767
+ id: m.id,
5768
+ label: m.label,
5769
+ vision: m.vision === true,
5770
+ ...m.contextWindow ? { contextWindow: m.contextWindow } : {},
5771
+ ...m.maxTokens ? { maxTokens: m.maxTokens } : {}
5772
+ });
5773
+ }
5774
+ }
5775
+ for (;;) {
5776
+ const id = await promptText(models.length === 0 ? "Model id" : "Add another model id (blank to finish)");
5777
+ if (!id) break;
5778
+ models.push({
5779
+ id,
5780
+ vision: false
5781
+ });
5782
+ }
5783
+ if (models.length === 0) {
5784
+ logger.print(" (no models selected — skipping)");
5785
+ return null;
5786
+ }
5787
+ if (preset.oauthKey) {
5788
+ const apiKeyLabel = "API key";
5789
+ const oauthLabel = "Log in with your subscription (OAuth — afw stores its own token)";
5790
+ const method = await promptChoice(`How should afw authenticate to ${preset.label}?`, [oauthLabel, apiKeyLabel]);
5791
+ if (method === oauthLabel) return registerOAuthProvider(preset, models);
5792
+ }
5793
+ const key = await promptSecret(`API key${preset.apiKeyUrl ? ` — get one at ${preset.apiKeyUrl}` : ""} (leave blank for none): `);
5794
+ const providerId = await promptText("Provider id", preset.id);
5795
+ return finalizeProvider({
5796
+ baseUrl: preset.baseUrl,
5797
+ api: preset.api,
5798
+ key,
5799
+ providerId,
5800
+ label: preset.label,
5801
+ models
5802
+ });
5803
+ }
5804
+ /** OAuth path: run afw's own subscription login, then register the provider
5805
+ * with `agent-oauth` auth (the token is resolved + refreshed from afw's own
5806
+ * store at request time) plus the selected models. */
5807
+ async function registerOAuthProvider(preset, models) {
5808
+ const def = await oauthLogin(preset.oauthKey);
5809
+ if (!def) {
5810
+ logger.print(" (login not completed — skipping this provider)");
5811
+ return null;
5812
+ }
5813
+ const providerId = await promptText("Provider id", preset.id);
5814
+ await daemonFetch("POST", "/api/routing/provider", {
5815
+ id: providerId,
5816
+ name: preset.label,
5817
+ baseUrl: def.register.baseUrl,
5818
+ api: def.register.api,
5819
+ authKind: "agent-oauth",
5820
+ agent: def.register.agent
5821
+ });
5822
+ for (const m of models) await daemonFetch("POST", "/api/routing/model", {
5823
+ id: m.id,
5824
+ providerId,
5825
+ ...m.label ? { label: m.label } : {},
5826
+ input: m.vision ? ["text", "image"] : ["text"],
5827
+ ...m.contextWindow ? { contextWindow: m.contextWindow } : {},
5828
+ ...m.maxTokens ? { maxTokens: m.maxTokens } : {}
5829
+ });
5830
+ logger.print(`✓ provider ${providerId} + ${models.length} model(s) registered (OAuth subscription)`);
5831
+ return {
5832
+ providerId,
5833
+ modelIds: models.map((m) => m.id)
5834
+ };
5835
+ }
5836
+ /** Manual path: ask for everything (base URL, wire format, models, vision). */
5837
+ async function addCustomProvider() {
5838
+ const baseUrl = await promptText("Provider base URL (e.g. https://api.openai.com/v1)");
4417
5839
  if (!baseUrl) {
4418
5840
  logger.print(" (no base URL — skipping)");
4419
5841
  return null;
@@ -4424,24 +5846,41 @@ async function addOneProvider() {
4424
5846
  "openai-responses",
4425
5847
  "anthropic"
4426
5848
  ]);
4427
- let key = await promptSecret("API key (leave blank for none): ");
4428
- const modelIds = [];
5849
+ const key = await promptSecret("API key (leave blank for none): ");
5850
+ const ids = [];
4429
5851
  for (;;) {
4430
- const id = await promptText(modelIds.length === 0 ? "Model id" : "Another model id (blank to finish)");
5852
+ const id = await promptText(ids.length === 0 ? "Model id" : "Another model id (blank to finish)");
4431
5853
  if (!id) break;
4432
- modelIds.push(id);
5854
+ ids.push(id);
4433
5855
  if (!process$1.stdin.isTTY) break;
4434
5856
  }
4435
- if (modelIds.length === 0) {
5857
+ if (ids.length === 0) {
4436
5858
  logger.print(" (no models — skipping)");
4437
5859
  return null;
4438
5860
  }
4439
5861
  const vision = await confirmYesNo("Do these models accept image input?", false);
4440
5862
  const providerId = await promptText("Provider id", providerIdFromUrl(baseUrl));
5863
+ return finalizeProvider({
5864
+ baseUrl,
5865
+ api: apiChoice,
5866
+ key,
5867
+ providerId,
5868
+ models: ids.map((id) => ({
5869
+ id,
5870
+ vision
5871
+ }))
5872
+ });
5873
+ }
5874
+ /** Shared tail of both paths: validate with a live probe (best-effort retry
5875
+ * loop), then register the provider + its models via the daemon. */
5876
+ async function finalizeProvider(p) {
5877
+ let baseUrl = p.baseUrl;
5878
+ let key = p.key;
5879
+ const apiChoice = p.api;
4441
5880
  let resolvedApi;
4442
5881
  for (;;) {
4443
5882
  logger.print(` probing ${baseUrl} …`);
4444
- const { result, api: api$1 } = await probe(apiChoice, baseUrl, key, modelIds[0]);
5883
+ const { result, api: api$1 } = await probe(apiChoice, baseUrl, key, p.models[0].id);
4445
5884
  if (result.ok && (api$1 ?? (apiChoice !== "auto" ? apiChoice : void 0))) {
4446
5885
  resolvedApi = api$1 ?? apiChoice;
4447
5886
  logger.print(` ✓ reachable (${resolvedApi}, HTTP ${result.status})`);
@@ -4465,28 +5904,32 @@ async function addOneProvider() {
4465
5904
  break;
4466
5905
  }
4467
5906
  if (retry === "edit base URL") baseUrl = await promptText("Provider base URL", baseUrl) || baseUrl;
4468
- if (retry === "edit model id") modelIds[0] = await promptText("Model id", modelIds[0]) || modelIds[0];
5907
+ if (retry === "edit model id") p.models[0].id = await promptText("Model id", p.models[0].id) || p.models[0].id;
4469
5908
  if (retry === "edit API key") key = await promptSecret("API key (leave blank for none): ");
4470
5909
  }
4471
5910
  const modelApi = API_TO_MODEL_API[resolvedApi ?? "openai-chat"];
4472
5911
  const authKind = key ? resolvedApi === "anthropic" ? "api-key" : "bearer" : "passthrough";
4473
5912
  await daemonFetch("POST", "/api/routing/provider", {
4474
- id: providerId,
5913
+ id: p.providerId,
5914
+ ...p.label ? { name: p.label } : {},
4475
5915
  baseUrl,
4476
5916
  api: modelApi,
4477
5917
  authKind,
4478
5918
  ...authKind === "api-key" ? { authHeader: "x-api-key" } : {},
4479
5919
  ...key ? { apiKey: key } : {}
4480
5920
  });
4481
- for (const id of modelIds) await daemonFetch("POST", "/api/routing/model", {
4482
- id,
4483
- providerId,
4484
- input: vision ? ["text", "image"] : ["text"]
5921
+ for (const m of p.models) await daemonFetch("POST", "/api/routing/model", {
5922
+ id: m.id,
5923
+ providerId: p.providerId,
5924
+ ...m.label ? { label: m.label } : {},
5925
+ input: m.vision ? ["text", "image"] : ["text"],
5926
+ ...m.contextWindow ? { contextWindow: m.contextWindow } : {},
5927
+ ...m.maxTokens ? { maxTokens: m.maxTokens } : {}
4485
5928
  });
4486
- logger.print(`✓ provider ${providerId} + ${modelIds.length} model(s) registered`);
5929
+ logger.print(`✓ provider ${p.providerId} + ${p.models.length} model(s) registered`);
4487
5930
  return {
4488
- providerId,
4489
- modelIds
5931
+ providerId: p.providerId,
5932
+ modelIds: p.models.map((m) => m.id)
4490
5933
  };
4491
5934
  }
4492
5935
  const addCmd$1 = new Command("add").description("Interactively add one or more model providers (validated with a live probe).").action(async () => {
@@ -4508,9 +5951,16 @@ const listCmd$2 = new Command("list").description("List registered providers and
4508
5951
  try {
4509
5952
  const reg = await daemonFetch("GET", "/api/routing/registry");
4510
5953
  logger.print("Providers:");
4511
- for (const p of reg.providers) logger.print(` ${p.id.padEnd(20)} ${p.api.padEnd(18)} ${p.baseUrl}`);
5954
+ for (const p of reg.providers) {
5955
+ const path$1 = `path=${p.generationPath ?? "versioned"}`;
5956
+ const effort = p.reasoningEffort ? ` effort=${p.reasoningEffort}` : "";
5957
+ logger.print(` ${p.id.padEnd(20)} ${p.api.padEnd(18)} ${path$1}${effort} ${p.baseUrl}`);
5958
+ }
4512
5959
  logger.print("Models:");
4513
- for (const m of reg.models) logger.print(` ${m.id.padEnd(28)} ${m.providerId}${m.input.includes("image") ? " (vision)" : ""}`);
5960
+ for (const m of reg.models) {
5961
+ const effort = m.reasoningEffort ? ` effort=${m.reasoningEffort}` : "";
5962
+ logger.print(` ${m.id.padEnd(28)} ${m.providerId}${m.input.includes("image") ? " (vision)" : ""}${effort}`);
5963
+ }
4514
5964
  if (reg.combos && reg.combos.length > 0) {
4515
5965
  logger.print("Fusion models:");
4516
5966
  for (const c of reg.combos) {
@@ -4523,7 +5973,7 @@ const listCmd$2 = new Command("list").description("List registered providers and
4523
5973
  process$1.exitCode = 1;
4524
5974
  }
4525
5975
  });
4526
- const modelCommand = new Command("model").description("Manage the providers, models, and API-key secrets afw can route to.").addCommand(addCmd$1).addCommand(listCmd$2).addCommand(modelRm).addCommand(providerCmd).addCommand(secretCmd);
5976
+ const modelCommand = new Command("model").description("Manage the providers, models, and API-key secrets afw can route to.").addCommand(addCmd$1).addCommand(listCmd$2).addCommand(modelRm).addCommand(modelSet).addCommand(providerCmd).addCommand(secretCmd);
4527
5977
 
4528
5978
  //#endregion
4529
5979
  //#region src/cli/launch/onboard.ts
@@ -8131,10 +9581,10 @@ var require_resolve_block_map = __commonJS({ "../../node_modules/yaml/dist/compo
8131
9581
  let offset = bm.offset;
8132
9582
  let commentEnd = null;
8133
9583
  for (const collItem of bm.items) {
8134
- const { start, key, sep, value } = collItem;
9584
+ const { start, key, sep: sep$1, value } = collItem;
8135
9585
  const keyProps = resolveProps$3.resolveProps(start, {
8136
9586
  indicator: "explicit-key-ind",
8137
- next: key ?? sep?.[0],
9587
+ next: key ?? sep$1?.[0],
8138
9588
  offset,
8139
9589
  onError,
8140
9590
  parentIndent: bm.indent,
@@ -8146,7 +9596,7 @@ var require_resolve_block_map = __commonJS({ "../../node_modules/yaml/dist/compo
8146
9596
  if (key.type === "block-seq") onError(offset, "BLOCK_AS_IMPLICIT_KEY", "A block sequence may not be used as an implicit map key");
8147
9597
  else if ("indent" in key && key.indent !== bm.indent) onError(offset, "BAD_INDENT", startColMsg);
8148
9598
  }
8149
- if (!keyProps.anchor && !keyProps.tag && !sep) {
9599
+ if (!keyProps.anchor && !keyProps.tag && !sep$1) {
8150
9600
  commentEnd = keyProps.end;
8151
9601
  if (keyProps.comment) if (map$6.comment) map$6.comment += "\n" + keyProps.comment;
8152
9602
  else map$6.comment = keyProps.comment;
@@ -8160,7 +9610,7 @@ var require_resolve_block_map = __commonJS({ "../../node_modules/yaml/dist/compo
8160
9610
  if (ctx.schema.compat) utilFlowIndentCheck$1.flowIndentCheck(bm.indent, key, onError);
8161
9611
  ctx.atKey = false;
8162
9612
  if (utilMapIncludes$1.mapIncludes(ctx, map$6.items, keyNode)) onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
8163
- const valueProps = resolveProps$3.resolveProps(sep ?? [], {
9613
+ const valueProps = resolveProps$3.resolveProps(sep$1 ?? [], {
8164
9614
  indicator: "map-value-ind",
8165
9615
  next: value,
8166
9616
  offset: keyNode.range[2],
@@ -8174,7 +9624,7 @@ var require_resolve_block_map = __commonJS({ "../../node_modules/yaml/dist/compo
8174
9624
  if (value?.type === "block-map" && !valueProps.hasNewline) onError(offset, "BLOCK_AS_IMPLICIT_KEY", "Nested mappings are not allowed in compact mappings");
8175
9625
  if (ctx.options.strict && keyProps.start < valueProps.found.offset - 1024) onError(keyNode.range, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit block mapping key");
8176
9626
  }
8177
- const valueNode = value ? composeNode$2(ctx, value, valueProps, onError) : composeEmptyNode$1(ctx, offset, sep, null, valueProps, onError);
9627
+ const valueNode = value ? composeNode$2(ctx, value, valueProps, onError) : composeEmptyNode$1(ctx, offset, sep$1, null, valueProps, onError);
8178
9628
  if (ctx.schema.compat) utilFlowIndentCheck$1.flowIndentCheck(bm.indent, value, onError);
8179
9629
  offset = valueNode.range[2];
8180
9630
  const pair = new Pair$2.Pair(keyNode, valueNode);
@@ -8251,7 +9701,7 @@ var require_resolve_end = __commonJS({ "../../node_modules/yaml/dist/compose/res
8251
9701
  let comment = "";
8252
9702
  if (end) {
8253
9703
  let hasSpace = false;
8254
- let sep = "";
9704
+ let sep$1 = "";
8255
9705
  for (const token of end) {
8256
9706
  const { source, type } = token;
8257
9707
  switch (type) {
@@ -8262,12 +9712,12 @@ var require_resolve_end = __commonJS({ "../../node_modules/yaml/dist/compose/res
8262
9712
  if (reqSpace && !hasSpace) onError(token, "MISSING_CHAR", "Comments must be separated from other tokens by white space characters");
8263
9713
  const cb = source.substring(1) || " ";
8264
9714
  if (!comment) comment = cb;
8265
- else comment += sep + cb;
8266
- sep = "";
9715
+ else comment += sep$1 + cb;
9716
+ sep$1 = "";
8267
9717
  break;
8268
9718
  }
8269
9719
  case "newline":
8270
- if (comment) sep += source;
9720
+ if (comment) sep$1 += source;
8271
9721
  hasSpace = true;
8272
9722
  break;
8273
9723
  default: onError(token, "UNEXPECTED_TOKEN", `Unexpected ${type} at node end`);
@@ -8308,18 +9758,18 @@ var require_resolve_flow_collection = __commonJS({ "../../node_modules/yaml/dist
8308
9758
  let offset = fc.offset + fc.start.source.length;
8309
9759
  for (let i = 0; i < fc.items.length; ++i) {
8310
9760
  const collItem = fc.items[i];
8311
- const { start, key, sep, value } = collItem;
9761
+ const { start, key, sep: sep$1, value } = collItem;
8312
9762
  const props = resolveProps$1.resolveProps(start, {
8313
9763
  flow: fcName,
8314
9764
  indicator: "explicit-key-ind",
8315
- next: key ?? sep?.[0],
9765
+ next: key ?? sep$1?.[0],
8316
9766
  offset,
8317
9767
  onError,
8318
9768
  parentIndent: fc.indent,
8319
9769
  startOnNewline: false
8320
9770
  });
8321
9771
  if (!props.found) {
8322
- if (!props.anchor && !props.tag && !sep && !value) {
9772
+ if (!props.anchor && !props.tag && !sep$1 && !value) {
8323
9773
  if (i === 0 && props.comma) onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`);
8324
9774
  else if (i < fc.items.length - 1) onError(props.start, "UNEXPECTED_TOKEN", `Unexpected empty item in ${fcName}`);
8325
9775
  if (props.comment) if (coll.comment) coll.comment += "\n" + props.comment;
@@ -8352,8 +9802,8 @@ var require_resolve_flow_collection = __commonJS({ "../../node_modules/yaml/dist
8352
9802
  }
8353
9803
  }
8354
9804
  }
8355
- if (!isMap$1 && !sep && !props.found) {
8356
- const valueNode = value ? composeNode$2(ctx, value, props, onError) : composeEmptyNode$1(ctx, props.end, sep, null, props, onError);
9805
+ if (!isMap$1 && !sep$1 && !props.found) {
9806
+ const valueNode = value ? composeNode$2(ctx, value, props, onError) : composeEmptyNode$1(ctx, props.end, sep$1, null, props, onError);
8357
9807
  coll.items.push(valueNode);
8358
9808
  offset = valueNode.range[2];
8359
9809
  if (isBlock(value)) onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg);
@@ -8363,7 +9813,7 @@ var require_resolve_flow_collection = __commonJS({ "../../node_modules/yaml/dist
8363
9813
  const keyNode = key ? composeNode$2(ctx, key, props, onError) : composeEmptyNode$1(ctx, keyStart, start, null, props, onError);
8364
9814
  if (isBlock(key)) onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg);
8365
9815
  ctx.atKey = false;
8366
- const valueProps = resolveProps$1.resolveProps(sep ?? [], {
9816
+ const valueProps = resolveProps$1.resolveProps(sep$1 ?? [], {
8367
9817
  flow: fcName,
8368
9818
  indicator: "map-value-ind",
8369
9819
  next: value,
@@ -8374,7 +9824,7 @@ var require_resolve_flow_collection = __commonJS({ "../../node_modules/yaml/dist
8374
9824
  });
8375
9825
  if (valueProps.found) {
8376
9826
  if (!isMap$1 && !props.found && ctx.options.strict) {
8377
- if (sep) for (const st of sep) {
9827
+ if (sep$1) for (const st of sep$1) {
8378
9828
  if (st === valueProps.found) break;
8379
9829
  if (st.type === "newline") {
8380
9830
  onError(st, "MULTILINE_IMPLICIT_KEY", "Implicit keys of flow sequence pairs need to be on a single line");
@@ -8385,7 +9835,7 @@ var require_resolve_flow_collection = __commonJS({ "../../node_modules/yaml/dist
8385
9835
  }
8386
9836
  } else if (value) if ("source" in value && value.source?.[0] === ":") onError(value, "MISSING_CHAR", `Missing space after : in ${fcName}`);
8387
9837
  else onError(valueProps.start, "MISSING_CHAR", `Missing , or : between ${fcName} items`);
8388
- const valueNode = value ? composeNode$2(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode$1(ctx, valueProps.end, sep, null, valueProps, onError) : null;
9838
+ const valueNode = value ? composeNode$2(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode$1(ctx, valueProps.end, sep$1, null, valueProps, onError) : null;
8389
9839
  if (valueNode) {
8390
9840
  if (isBlock(value)) onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg);
8391
9841
  } else if (valueProps.comment) if (keyNode.comment) keyNode.comment += "\n" + valueProps.comment;
@@ -8560,7 +10010,7 @@ var require_resolve_block_scalar = __commonJS({ "../../node_modules/yaml/dist/co
8560
10010
  }
8561
10011
  for (let i = lines.length - 1; i >= chompStart; --i) if (lines[i][0].length > trimIndent) chompStart = i + 1;
8562
10012
  let value = "";
8563
- let sep = "";
10013
+ let sep$1 = "";
8564
10014
  let prevMoreIndented = false;
8565
10015
  for (let i = 0; i < contentStart; ++i) value += lines[i][0].slice(trimIndent) + "\n";
8566
10016
  for (let i = contentStart; i < chompStart; ++i) {
@@ -8576,19 +10026,19 @@ var require_resolve_block_scalar = __commonJS({ "../../node_modules/yaml/dist/co
8576
10026
  indent = "";
8577
10027
  }
8578
10028
  if (type === Scalar$3.Scalar.BLOCK_LITERAL) {
8579
- value += sep + indent.slice(trimIndent) + content;
8580
- sep = "\n";
10029
+ value += sep$1 + indent.slice(trimIndent) + content;
10030
+ sep$1 = "\n";
8581
10031
  } else if (indent.length > trimIndent || content[0] === " ") {
8582
- if (sep === " ") sep = "\n";
8583
- else if (!prevMoreIndented && sep === "\n") sep = "\n\n";
8584
- value += sep + indent.slice(trimIndent) + content;
8585
- sep = "\n";
10032
+ if (sep$1 === " ") sep$1 = "\n";
10033
+ else if (!prevMoreIndented && sep$1 === "\n") sep$1 = "\n\n";
10034
+ value += sep$1 + indent.slice(trimIndent) + content;
10035
+ sep$1 = "\n";
8586
10036
  prevMoreIndented = true;
8587
- } else if (content === "") if (sep === "\n") value += "\n";
8588
- else sep = "\n";
10037
+ } else if (content === "") if (sep$1 === "\n") value += "\n";
10038
+ else sep$1 = "\n";
8589
10039
  else {
8590
- value += sep + content;
8591
- sep = " ";
10040
+ value += sep$1 + content;
10041
+ sep$1 = " ";
8592
10042
  prevMoreIndented = false;
8593
10043
  }
8594
10044
  }
@@ -8782,22 +10232,22 @@ var require_resolve_flow_scalar = __commonJS({ "../../node_modules/yaml/dist/com
8782
10232
  let match$1 = first.exec(source);
8783
10233
  if (!match$1) return source;
8784
10234
  let res = match$1[1];
8785
- let sep = " ";
10235
+ let sep$1 = " ";
8786
10236
  let pos = first.lastIndex;
8787
10237
  line.lastIndex = pos;
8788
10238
  while (match$1 = line.exec(source)) {
8789
- if (match$1[1] === "") if (sep === "\n") res += sep;
8790
- else sep = "\n";
10239
+ if (match$1[1] === "") if (sep$1 === "\n") res += sep$1;
10240
+ else sep$1 = "\n";
8791
10241
  else {
8792
- res += sep + match$1[1];
8793
- sep = " ";
10242
+ res += sep$1 + match$1[1];
10243
+ sep$1 = " ";
8794
10244
  }
8795
10245
  pos = line.lastIndex;
8796
10246
  }
8797
10247
  const last = /[ \t]*(.*)/sy;
8798
10248
  last.lastIndex = pos;
8799
10249
  match$1 = last.exec(source);
8800
- return res + sep + (match$1?.[1] ?? "");
10250
+ return res + sep$1 + (match$1?.[1] ?? "");
8801
10251
  }
8802
10252
  function doubleQuotedValue(source, onError) {
8803
10253
  let res = "";
@@ -9633,11 +11083,11 @@ var require_cst_stringify = __commonJS({ "../../node_modules/yaml/dist/parse/cst
9633
11083
  }
9634
11084
  }
9635
11085
  }
9636
- function stringifyItem({ start, key, sep, value }) {
11086
+ function stringifyItem({ start, key, sep: sep$1, value }) {
9637
11087
  let res = "";
9638
11088
  for (const st of start) res += st.source;
9639
11089
  if (key) res += stringifyToken(key);
9640
- if (sep) for (const st of sep) res += st.source;
11090
+ if (sep$1) for (const st of sep$1) res += st.source;
9641
11091
  if (value) res += stringifyToken(value);
9642
11092
  return res;
9643
11093
  }
@@ -10772,12 +12222,12 @@ var require_parser = __commonJS({ "../../node_modules/yaml/dist/parse/parser.js"
10772
12222
  if (this.type === "map-value-ind") {
10773
12223
  const prev = getPrevProps(this.peek(2));
10774
12224
  const start = getFirstKeyStartProps(prev);
10775
- let sep;
12225
+ let sep$1;
10776
12226
  if (scalar.end) {
10777
- sep = scalar.end;
10778
- sep.push(this.sourceToken);
12227
+ sep$1 = scalar.end;
12228
+ sep$1.push(this.sourceToken);
10779
12229
  delete scalar.end;
10780
- } else sep = [this.sourceToken];
12230
+ } else sep$1 = [this.sourceToken];
10781
12231
  const map$6 = {
10782
12232
  type: "block-map",
10783
12233
  offset: scalar.offset,
@@ -10785,7 +12235,7 @@ var require_parser = __commonJS({ "../../node_modules/yaml/dist/parse/parser.js"
10785
12235
  items: [{
10786
12236
  start,
10787
12237
  key: scalar,
10788
- sep
12238
+ sep: sep$1
10789
12239
  }]
10790
12240
  };
10791
12241
  this.onKeyLine = true;
@@ -10937,8 +12387,8 @@ var require_parser = __commonJS({ "../../node_modules/yaml/dist/parse/parser.js"
10937
12387
  else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) {
10938
12388
  const start$1 = getFirstKeyStartProps(it.start);
10939
12389
  const key = it.key;
10940
- const sep = it.sep;
10941
- sep.push(this.sourceToken);
12390
+ const sep$1 = it.sep;
12391
+ sep$1.push(this.sourceToken);
10942
12392
  delete it.key;
10943
12393
  delete it.sep;
10944
12394
  this.stack.push({
@@ -10948,7 +12398,7 @@ var require_parser = __commonJS({ "../../node_modules/yaml/dist/parse/parser.js"
10948
12398
  items: [{
10949
12399
  start: start$1,
10950
12400
  key,
10951
- sep
12401
+ sep: sep$1
10952
12402
  }]
10953
12403
  });
10954
12404
  } else if (start.length > 0) it.sep = it.sep.concat(start, this.sourceToken);
@@ -11143,8 +12593,8 @@ var require_parser = __commonJS({ "../../node_modules/yaml/dist/parse/parser.js"
11143
12593
  const prev = getPrevProps(parent);
11144
12594
  const start = getFirstKeyStartProps(prev);
11145
12595
  fixFlowSeqItems(fc);
11146
- const sep = fc.end.splice(1, fc.end.length);
11147
- sep.push(this.sourceToken);
12596
+ const sep$1 = fc.end.splice(1, fc.end.length);
12597
+ sep$1.push(this.sourceToken);
11148
12598
  const map$6 = {
11149
12599
  type: "block-map",
11150
12600
  offset: fc.offset,
@@ -11152,7 +12602,7 @@ var require_parser = __commonJS({ "../../node_modules/yaml/dist/parse/parser.js"
11152
12602
  items: [{
11153
12603
  start,
11154
12604
  key: fc,
11155
- sep
12605
+ sep: sep$1
11156
12606
  }]
11157
12607
  };
11158
12608
  this.onKeyLine = true;
@@ -11412,245 +12862,9 @@ var require_dist = __commonJS({ "../../node_modules/yaml/dist/index.js"(exports)
11412
12862
  } });
11413
12863
  var import_dist = __toESM(require_dist(), 1);
11414
12864
 
11415
- //#endregion
11416
- //#region src/daemon/orchestrator/oauth/lock.ts
11417
- const STALE_MS = 3e4;
11418
- const MAX_WAIT_MS = 5e3;
11419
- const POLL_MS = 100;
11420
- async function withFileLock(path$1, fn) {
11421
- const held = await acquire(path$1);
11422
- try {
11423
- return await fn();
11424
- } finally {
11425
- if (held) await unlink(path$1).catch(() => {});
11426
- }
11427
- }
11428
- async function acquire(path$1) {
11429
- const deadline = Date.now() + MAX_WAIT_MS;
11430
- for (;;) try {
11431
- const fh = await open(path$1, "wx");
11432
- await fh.close();
11433
- return true;
11434
- } catch (err) {
11435
- if (err.code !== "EEXIST") return false;
11436
- try {
11437
- const st = await stat(path$1);
11438
- if (Date.now() - st.mtimeMs > STALE_MS) {
11439
- await unlink(path$1).catch(() => {});
11440
- continue;
11441
- }
11442
- } catch {
11443
- continue;
11444
- }
11445
- if (Date.now() > deadline) return false;
11446
- await new Promise((r) => setTimeout(r, POLL_MS));
11447
- }
11448
- }
11449
-
11450
- //#endregion
11451
- //#region src/daemon/orchestrator/oauth/claude-code.ts
11452
- const execFileP = promisify(execFile);
11453
- const KEYCHAIN_SERVICE = "Claude Code-credentials";
11454
- const CREDENTIALS_FILE = join(homedir(), ".claude", ".credentials.json");
11455
- const TOKEN_URL$1 = "https://platform.claude.com/v1/oauth/token";
11456
- const CLIENT_ID$1 = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
11457
- const SCOPE = "user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
11458
- const EXPIRY_BUFFER_MS$1 = 5 * 60 * 1e3;
11459
- /** The ~/.claude/.credentials.json store (also the test seam). */
11460
- function fileStore(path$1 = CREDENTIALS_FILE) {
11461
- return {
11462
- label: `file ${path$1}`,
11463
- async read() {
11464
- try {
11465
- return JSON.parse(await readFile(path$1, "utf8"));
11466
- } catch {
11467
- return void 0;
11468
- }
11469
- },
11470
- async write(creds) {
11471
- await atomicWrite(path$1, `${JSON.stringify(creds, null, 2)}\n`, { mode: 384 });
11472
- }
11473
- };
11474
- }
11475
- async function runSecurity(args) {
11476
- try {
11477
- const { stdout } = await execFileP("security", args);
11478
- return stdout;
11479
- } catch {
11480
- return void 0;
11481
- }
11482
- }
11483
- /** `security -w` returns the password verbatim when printable; for a blob it
11484
- * may hex-encode it. Claude Code stores JSON, so accept either form. */
11485
- function decodeKeychainValue(raw$1) {
11486
- const v = raw$1.trim();
11487
- if (v === "") return void 0;
11488
- if (v.startsWith("{")) return v;
11489
- if (/^[0-9a-fA-F]+$/.test(v) && v.length % 2 === 0) try {
11490
- const decoded = Buffer$1.from(v, "hex").toString("utf8");
11491
- if (decoded.startsWith("{")) return decoded;
11492
- } catch {}
11493
- return v;
11494
- }
11495
- async function keychainAccount() {
11496
- try {
11497
- const { stdout, stderr } = await execFileP("security", [
11498
- "find-generic-password",
11499
- "-s",
11500
- KEYCHAIN_SERVICE,
11501
- "-g"
11502
- ]);
11503
- const m = `${stderr}${stdout}`.match(/"acct"<blob>=(?:0x[0-9A-Fa-f]+\s+)?"([^"]*)"/);
11504
- return m?.[1];
11505
- } catch {
11506
- return void 0;
11507
- }
11508
- }
11509
- function keychainStore() {
11510
- let account = "";
11511
- return {
11512
- label: `keychain ${KEYCHAIN_SERVICE}`,
11513
- async read() {
11514
- const pw = await runSecurity([
11515
- "find-generic-password",
11516
- "-s",
11517
- KEYCHAIN_SERVICE,
11518
- "-w"
11519
- ]);
11520
- if (pw === void 0) return void 0;
11521
- const json = decodeKeychainValue(pw);
11522
- if (!json) return void 0;
11523
- try {
11524
- const creds = JSON.parse(json);
11525
- account = await keychainAccount() ?? "";
11526
- return creds;
11527
- } catch {
11528
- return void 0;
11529
- }
11530
- },
11531
- async write(creds) {
11532
- if (account === "") {
11533
- logger.warn("oauth: claude-code keychain account unknown; skipped token write-back");
11534
- return;
11535
- }
11536
- try {
11537
- await execFileP("security", [
11538
- "add-generic-password",
11539
- "-U",
11540
- "-a",
11541
- account,
11542
- "-s",
11543
- KEYCHAIN_SERVICE,
11544
- "-w",
11545
- JSON.stringify(creds)
11546
- ]);
11547
- } catch (err) {
11548
- logger.warn(`oauth: claude-code keychain write-back failed — ${err.message}`);
11549
- }
11550
- }
11551
- };
11552
- }
11553
- async function keychainHasItem() {
11554
- try {
11555
- await execFileP("security", [
11556
- "find-generic-password",
11557
- "-s",
11558
- KEYCHAIN_SERVICE
11559
- ]);
11560
- return true;
11561
- } catch {
11562
- return false;
11563
- }
11564
- }
11565
- /** Pick the live credential store: Keychain on macOS, else the JSON file. */
11566
- async function pickStore() {
11567
- if (process$1.platform === "darwin" && await keychainHasItem()) return keychainStore();
11568
- if (await fileExists(CREDENTIALS_FILE)) return fileStore();
11569
- return void 0;
11570
- }
11571
- /** Exchange a refresh token for a fresh access token. The rotated refresh
11572
- * token (when the provider returns one) is in the result and must be
11573
- * written back to the agent's store by the caller. */
11574
- async function refreshClaudeToken(refreshToken) {
11575
- const res = await fetch(TOKEN_URL$1, {
11576
- method: "POST",
11577
- headers: { "content-type": "application/json" },
11578
- body: JSON.stringify({
11579
- grant_type: "refresh_token",
11580
- refresh_token: refreshToken,
11581
- client_id: CLIENT_ID$1,
11582
- scope: SCOPE
11583
- })
11584
- });
11585
- if (!res.ok) throw new Error(`claude-code OAuth refresh failed: HTTP ${res.status}`);
11586
- const j = await res.json();
11587
- if (!j.access_token) throw new Error("claude-code OAuth refresh: response carried no access_token");
11588
- return {
11589
- accessToken: j.access_token,
11590
- refreshToken: j.refresh_token ?? refreshToken,
11591
- expiresAt: Date.now() + (j.expires_in ?? 3600) * 1e3
11592
- };
11593
- }
11594
- /** Resolve a usable access token from `store`, refreshing + writing back the
11595
- * rotated token when the stored one is within the expiry buffer. No
11596
- * in-memory cache and no lock — the production entry point adds those; this
11597
- * is the unit-test seam. */
11598
- async function resolveClaudeToken(store$1) {
11599
- const creds = await store$1.read();
11600
- const oauth = creds?.claudeAiOauth;
11601
- if (!creds || !oauth?.accessToken || !oauth.refreshToken) throw new Error(`claude-code OAuth credentials unavailable (${store$1.label})`);
11602
- const expiresAt = typeof oauth.expiresAt === "number" ? oauth.expiresAt : 0;
11603
- if (Date.now() < expiresAt - EXPIRY_BUFFER_MS$1) return {
11604
- token: oauth.accessToken,
11605
- expiresAt
11606
- };
11607
- const refreshed = await refreshClaudeToken(oauth.refreshToken);
11608
- const updated = {
11609
- ...creds,
11610
- claudeAiOauth: {
11611
- ...oauth,
11612
- accessToken: refreshed.accessToken,
11613
- refreshToken: refreshed.refreshToken,
11614
- expiresAt: refreshed.expiresAt
11615
- }
11616
- };
11617
- await store$1.write(updated);
11618
- return {
11619
- token: refreshed.accessToken,
11620
- expiresAt: refreshed.expiresAt
11621
- };
11622
- }
11623
- let cache$1;
11624
- let inflight$1;
11625
- /** True when Claude Code has subscription OAuth credentials on this machine
11626
- * — used at wire time to mark the route `agent-oauth`. */
11627
- async function claudeCodeOAuthAvailable() {
11628
- const store$1 = await pickStore();
11629
- if (!store$1) return false;
11630
- const creds = await store$1.read();
11631
- return typeof creds?.claudeAiOauth?.refreshToken === "string";
11632
- }
11633
- /** The orchestrator entry point: a fresh access token, cached in memory and
11634
- * refreshed at most once across concurrent callers. */
11635
- async function getClaudeCodeToken() {
11636
- if (cache$1 && Date.now() < cache$1.expiresAt - EXPIRY_BUFFER_MS$1) return { token: cache$1.token };
11637
- if (inflight$1) return inflight$1;
11638
- inflight$1 = (async () => {
11639
- const store$1 = await pickStore();
11640
- if (!store$1) throw new Error("claude-code OAuth credentials not found");
11641
- const lockPath = join(paths.home, "oauth-claude-code.lock");
11642
- const resolved = await withFileLock(lockPath, () => resolveClaudeToken(store$1));
11643
- cache$1 = resolved;
11644
- return resolved;
11645
- })().finally(() => {
11646
- inflight$1 = void 0;
11647
- });
11648
- return inflight$1;
11649
- }
11650
-
11651
12865
  //#endregion
11652
12866
  //#region src/cli/detect/credentials.ts
11653
- function isObj$9(v) {
12867
+ function isObj$8(v) {
11654
12868
  return typeof v === "object" && v !== null && !Array.isArray(v);
11655
12869
  }
11656
12870
  /** The auth header convention for a wire decoder: Anthropic carries the key
@@ -11702,7 +12916,7 @@ async function readJsonFile(path$1) {
11702
12916
  * literal credential. Returns undefined when nothing resolves.
11703
12917
  */
11704
12918
  function resolveSecretInput(raw$1, env) {
11705
- if (isObj$9(raw$1)) {
12919
+ if (isObj$8(raw$1)) {
11706
12920
  if (raw$1.source === "env" && typeof raw$1.id === "string") {
11707
12921
  const v = env[raw$1.id];
11708
12922
  return v && v.trim() !== "" ? v.trim() : void 0;
@@ -11759,21 +12973,13 @@ async function captureClaudeCodeCredentials(opts) {
11759
12973
  }
11760
12974
  const legacy = await readJsonFile(opts?.legacyPath ?? paths.agent.claudeCode.legacy);
11761
12975
  const primary = typeof legacy?.primaryApiKey === "string" ? legacy.primaryApiKey.trim() : "";
11762
- if (primary !== "") {
11763
- out.set("anthropic", {
11764
- auth: {
11765
- kind: "api-key",
11766
- header: "x-api-key"
11767
- },
11768
- value: primary
11769
- });
11770
- return out;
11771
- }
11772
- const oauthProbe = opts?.oauthProbe ?? claudeCodeOAuthAvailable;
11773
- if (await oauthProbe()) out.set("anthropic", { auth: {
11774
- kind: "agent-oauth",
11775
- agent: "claude-code"
11776
- } });
12976
+ if (primary !== "") out.set("anthropic", {
12977
+ auth: {
12978
+ kind: "api-key",
12979
+ header: "x-api-key"
12980
+ },
12981
+ value: primary
12982
+ });
11777
12983
  return out;
11778
12984
  }
11779
12985
  /** Codex — the API-key-mode credential is a literal `OPENAI_API_KEY` in
@@ -11783,17 +12989,10 @@ async function captureCodexCredentials(opts) {
11783
12989
  const out = new Map();
11784
12990
  const auth = await readJsonFile(opts?.authPath ?? paths.agent.codex.auth);
11785
12991
  const key = typeof auth?.OPENAI_API_KEY === "string" ? auth.OPENAI_API_KEY.trim() : "";
11786
- if (key !== "") {
11787
- out.set("openai", {
11788
- auth: { kind: "bearer" },
11789
- value: key
11790
- });
11791
- return out;
11792
- }
11793
- if (auth?.auth_mode === "chatgpt" && isObj$9(auth.tokens)) out.set("openai", { auth: {
11794
- kind: "agent-oauth",
11795
- agent: "codex"
11796
- } });
12992
+ if (key !== "") out.set("openai", {
12993
+ auth: { kind: "bearer" },
12994
+ value: key
12995
+ });
11797
12996
  return out;
11798
12997
  }
11799
12998
  /** Hermes — `model.api_key` and `custom_providers[].api_key` in
@@ -11806,15 +13005,15 @@ async function captureHermesCredentials(endpoints, opts) {
11806
13005
  } catch {
11807
13006
  return out;
11808
13007
  }
11809
- if (!isObj$9(doc)) return out;
13008
+ if (!isObj$8(doc)) return out;
11810
13009
  const env = await readDotEnv(opts?.envPath ?? paths.agent.hermes.env);
11811
13010
  const customSeq = Array.isArray(doc.custom_providers) ? doc.custom_providers : [];
11812
13011
  for (const ep of endpoints) {
11813
13012
  let rawKey;
11814
- if (ep.configLocation === "/model/base_url") rawKey = isObj$9(doc.model) ? doc.model.api_key : void 0;
13013
+ if (ep.configLocation === "/model/base_url") rawKey = isObj$8(doc.model) ? doc.model.api_key : void 0;
11815
13014
  else if (ep.configLocation.startsWith("/custom_providers/")) {
11816
- const found = customSeq.find((p) => isObj$9(p) && p.name === ep.modelId);
11817
- rawKey = isObj$9(found) ? found.api_key : void 0;
13015
+ const found = customSeq.find((p) => isObj$8(p) && p.name === ep.modelId);
13016
+ rawKey = isObj$8(found) ? found.api_key : void 0;
11818
13017
  }
11819
13018
  const value = resolveSecretInput(rawKey, env);
11820
13019
  if (value !== void 0) out.set(ep.modelId, {
@@ -11865,7 +13064,7 @@ async function captureOpenClawCredentials(endpoints, opts) {
11865
13064
  const agentId = opts?.agentId ?? "main";
11866
13065
  for (const ep of endpoints) {
11867
13066
  const provider = providers[ep.modelId];
11868
- if (!isObj$9(provider)) continue;
13067
+ if (!isObj$8(provider)) continue;
11869
13068
  const profileKey = await readOpenClawAuthProfileKey(ep.modelId, agentId, configPath);
11870
13069
  if (profileKey) {
11871
13070
  out.set(ep.modelId, {
@@ -12028,9 +13227,9 @@ function setTomlTopLevelString(text$1, key, value) {
12028
13227
  sectionAdded: false,
12029
13228
  keyAdded: true
12030
13229
  };
12031
- const sep = isHeaderLine(text$1.split("\n", 1)[0] ?? "") ? "\n\n" : "\n";
13230
+ const sep$1 = isHeaderLine(text$1.split("\n", 1)[0] ?? "") ? "\n\n" : "\n";
12032
13231
  return {
12033
- text: `${newLine}${sep}${text$1}`,
13232
+ text: `${newLine}${sep$1}${text$1}`,
12034
13233
  sectionAdded: false,
12035
13234
  keyAdded: true
12036
13235
  };
@@ -12472,27 +13671,6 @@ async function revertEntries(entries, agent, opts) {
12472
13671
  return { skipped };
12473
13672
  }
12474
13673
 
12475
- //#endregion
12476
- //#region src/cli/wire/decoder-for.ts
12477
- /**
12478
- * Heuristic decoder selection based on upstream URL. Users can override per
12479
- * route in `~/.afw/wire/routes.json`.
12480
- */
12481
- function decoderFor$1(upstream) {
12482
- let host;
12483
- try {
12484
- host = new URL(upstream).hostname;
12485
- } catch {
12486
- return "passthrough";
12487
- }
12488
- if (host.endsWith("anthropic.com")) return "anthropic";
12489
- if (host.endsWith("openai.com")) return "openai-chat";
12490
- if (host === "openrouter.ai") return "openai-chat";
12491
- if (host.endsWith("googleapis.com")) return "gemini";
12492
- if (host.endsWith("amazonaws.com")) return "bedrock";
12493
- return "openai-chat";
12494
- }
12495
-
12496
13674
  //#endregion
12497
13675
  //#region src/cli/detect/mcp.ts
12498
13676
  /**
@@ -12636,10 +13814,10 @@ function safeRealpath(p) {
12636
13814
  * here as one function instead of being copy-pasted per detector.
12637
13815
  */
12638
13816
  async function injectAfwToolsMcp(agent, filePath) {
12639
- const exists$1 = await fileExists(filePath);
13817
+ const exists$2 = await fileExists(filePath);
12640
13818
  let originalText = "";
12641
13819
  let parsed;
12642
- if (exists$1) {
13820
+ if (exists$2) {
12643
13821
  originalText = await readFile(filePath, "utf8");
12644
13822
  parsed = parseJsonc(originalText);
12645
13823
  const existing = parsed?.mcpServers?.[AFW_TOOLS_MCP_KEY];
@@ -12653,7 +13831,7 @@ async function injectAfwToolsMcp(agent, filePath) {
12653
13831
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
12654
13832
  const backupDir = join(paths.backups.dir, ts, agent);
12655
13833
  const backupPath = join(backupDir, basename(filePath));
12656
- if (exists$1) await backupCopy(filePath, backupPath);
13834
+ if (exists$2) await backupCopy(filePath, backupPath);
12657
13835
  else await atomicWrite(backupPath, "");
12658
13836
  const originalSha = sha256OfString(originalText);
12659
13837
  let text$1 = originalText === "" ? "{}\n" : originalText;
@@ -12691,6 +13869,9 @@ async function injectAfwToolsMcp(agent, filePath) {
12691
13869
  //#region src/cli/detect/claude-code.ts
12692
13870
  const AGENT$4 = "claude-code";
12693
13871
  const ANTHROPIC_DEFAULT$1 = "https://api.anthropic.com";
13872
+ function decoderForClaudeCodeBaseUrl(_upstream) {
13873
+ return "anthropic";
13874
+ }
12694
13875
  const claudeCodeDetector = {
12695
13876
  agent: AGENT$4,
12696
13877
  mode: "launch-per-task",
@@ -12716,7 +13897,7 @@ const claudeCodeDetector = {
12716
13897
  originalBaseUrl: currentBaseUrl,
12717
13898
  afwBaseUrl: afwBaseUrl$1,
12718
13899
  upstream,
12719
- decoder: decoderFor$1(upstream),
13900
+ decoder: decoderForClaudeCodeBaseUrl(upstream),
12720
13901
  configLocation: "/env/ANTHROPIC_BASE_URL",
12721
13902
  filePath: settingsPath,
12722
13903
  active: true
@@ -12976,7 +14157,7 @@ const claudeDesktopDetector = {
12976
14157
  }];
12977
14158
  const ccCred = (await captureClaudeCodeCredentials()).get("anthropic");
12978
14159
  if (ccCred) endpoints[0].auth = ccCred.auth;
12979
- else caveats.push("no Anthropic credential found via claude-code — wire claude-code first (e.g. set ANTHROPIC_API_KEY in ~/.claude/settings.json env, or sign in to Claude.ai via `claude /login`). Until then, Claude Desktop requests will be rejected by Anthropic with 401.");
14160
+ else caveats.push("no Anthropic API key found via claude-code — set ANTHROPIC_API_KEY in ~/.claude/settings.json env, or run `afw oauth login anthropic` to route through a Claude.ai subscription. Until then, Claude Desktop requests will be rejected by Anthropic with 401.");
12980
14161
  return {
12981
14162
  agent: AGENT$3,
12982
14163
  mode: "manual",
@@ -13305,6 +14486,27 @@ function execCapture(cmd, args) {
13305
14486
  });
13306
14487
  }
13307
14488
 
14489
+ //#endregion
14490
+ //#region src/cli/wire/decoder-for.ts
14491
+ /**
14492
+ * Heuristic decoder selection based on upstream URL. Users can override per
14493
+ * route in `~/.afw/wire/routes.json`.
14494
+ */
14495
+ function decoderFor$1(upstream) {
14496
+ let host;
14497
+ try {
14498
+ host = new URL(upstream).hostname;
14499
+ } catch {
14500
+ return "passthrough";
14501
+ }
14502
+ if (host.endsWith("anthropic.com")) return "anthropic";
14503
+ if (host.endsWith("openai.com")) return "openai-chat";
14504
+ if (host === "openrouter.ai") return "openai-chat";
14505
+ if (host.endsWith("googleapis.com")) return "gemini";
14506
+ if (host.endsWith("amazonaws.com")) return "bedrock";
14507
+ return "openai-chat";
14508
+ }
14509
+
13308
14510
  //#endregion
13309
14511
  //#region src/cli/wire/routes.ts
13310
14512
  async function readRoutes() {
@@ -13768,7 +14970,7 @@ function findWrapTarget(c) {
13768
14970
  originalPrimary: pick.originalPrimary
13769
14971
  };
13770
14972
  }
13771
- const MODALITIES$2 = new Set([
14973
+ const MODALITIES$1 = new Set([
13772
14974
  "text",
13773
14975
  "audio",
13774
14976
  "image",
@@ -13794,7 +14996,7 @@ function harvestOpenClawModels(raw$1) {
13794
14996
  const rec = m;
13795
14997
  const id = typeof rec.id === "string" ? rec.id.trim() : "";
13796
14998
  if (!id) continue;
13797
- const input = Array.isArray(rec.input) ? rec.input.filter((x) => MODALITIES$2.has(x)) : [];
14999
+ const input = Array.isArray(rec.input) ? rec.input.filter((x) => MODALITIES$1.has(x)) : [];
13798
15000
  const cost = harvestCost(rec.cost);
13799
15001
  out.push({
13800
15002
  id,
@@ -14224,16 +15426,17 @@ const FALLBACK$1 = {
14224
15426
  decoder: "openai-responses"
14225
15427
  }
14226
15428
  };
14227
- async function ensureWireRoute(agent) {
15429
+ async function ensureWireRoute(agent, opts) {
14228
15430
  const det = detectorFor(agent);
14229
15431
  const detection = det ? await det.detect() : null;
15432
+ const codexDecoder = agent === "codex" ? (await resolveCodexWireProtocol(opts?.modelOverride)).decoder : void 0;
14230
15433
  const routeUpdates = {};
14231
15434
  if (detection) {
14232
15435
  for (const ep of detection.endpoints) {
14233
15436
  if (!ep.active) continue;
14234
15437
  routeUpdates[routeKeyForModel(agent, ep.modelId)] = {
14235
15438
  upstream: ep.upstream,
14236
- decoder: ep.decoder,
15439
+ decoder: codexDecoder ?? ep.decoder,
14237
15440
  ...ep.sourceModelId ? { sourceModelId: ep.sourceModelId } : {},
14238
15441
  ...ep.harvest ? { harvest: ep.harvest } : {},
14239
15442
  ...ep.auth ? { auth: ep.auth } : {}
@@ -14249,7 +15452,7 @@ async function ensureWireRoute(agent) {
14249
15452
  const fb = FALLBACK$1[agent];
14250
15453
  if (fb) routeUpdates[routeKeyForModel(agent, "*")] = {
14251
15454
  upstream: fb.upstream,
14252
- decoder: fb.decoder
15455
+ decoder: codexDecoder ?? fb.decoder
14253
15456
  };
14254
15457
  }
14255
15458
  if (Object.keys(routeUpdates).length > 0) await upsertRoutes(routeUpdates);
@@ -14395,7 +15598,7 @@ function buildLauncher(agent, bin) {
14395
15598
  };
14396
15599
  if (flagGiven) await writeLaunchConfig(cwd, agent, cfg);
14397
15600
  await ensureDaemonRunning();
14398
- await ensureWireRoute(agent);
15601
+ await ensureWireRoute(agent, { modelOverride: cfg.model });
14399
15602
  const instanceLabel = cfg.as ?? dirLabel(cwd);
14400
15603
  if (cfg.mode !== "raw") try {
14401
15604
  const { key, created } = await ensureSessionKey(agent, instanceIdFrom(instanceLabel));
@@ -14982,8 +16185,8 @@ var createAdaptorServer = (options) => {
14982
16185
  overrideGlobalObjects: options.overrideGlobalObjects,
14983
16186
  autoCleanupIncoming: options.autoCleanupIncoming
14984
16187
  });
14985
- const createServer$1 = options.createServer || createServer;
14986
- const server = createServer$1(options.serverOptions || {}, requestListener);
16188
+ const createServer$2 = options.createServer || createServer$1;
16189
+ const server = createServer$2(options.serverOptions || {}, requestListener);
14987
16190
  return server;
14988
16191
  };
14989
16192
  var serve = (options, listeningListener) => {
@@ -22384,11 +23587,11 @@ function generateToken(taken = []) {
22384
23587
  }
22385
23588
  return randToken();
22386
23589
  }
22387
- function isObj$8(v) {
23590
+ function isObj$7(v) {
22388
23591
  return typeof v === "object" && v !== null && !Array.isArray(v);
22389
23592
  }
22390
23593
  function normalizeEntry(raw$1) {
22391
- if (!isObj$8(raw$1)) return void 0;
23594
+ if (!isObj$7(raw$1)) return void 0;
22392
23595
  const id = typeof raw$1.id === "string" ? raw$1.id.trim() : "";
22393
23596
  const token = typeof raw$1.token === "string" ? raw$1.token.trim() : "";
22394
23597
  if (!id || !token) return void 0;
@@ -22403,7 +23606,7 @@ function normalizeEntry(raw$1) {
22403
23606
  };
22404
23607
  }
22405
23608
  function normalizeAccessKeys(raw$1) {
22406
- if (!isObj$8(raw$1)) return { ...EMPTY_ACCESS_KEYS };
23609
+ if (!isObj$7(raw$1)) return { ...EMPTY_ACCESS_KEYS };
22407
23610
  if (raw$1.version !== ACCESS_KEYS_VERSION) throw new Error(`keys.json version ${String(raw$1.version)} not supported (expected ${ACCESS_KEYS_VERSION})`);
22408
23611
  const keys = [];
22409
23612
  if (Array.isArray(raw$1.keys)) for (const entry of raw$1.keys) {
@@ -22422,16 +23625,16 @@ async function readAccessKeys() {
22422
23625
  async function writeAccessKeys(store$1) {
22423
23626
  await atomicWrite(paths.keys, `${JSON.stringify(store$1, null, 2)}\n`, { mode: KEYS_MODE });
22424
23627
  }
22425
- let writeChain$3 = Promise.resolve();
23628
+ let writeChain$2 = Promise.resolve();
22426
23629
  /** Serialized read-modify-write — see model-registry.ts mutateModelRegistry. */
22427
23630
  function mutateAccessKeys(fn) {
22428
- const next = writeChain$3.then(async () => {
23631
+ const next = writeChain$2.then(async () => {
22429
23632
  const store$1 = await readAccessKeys();
22430
23633
  const updated = fn(store$1);
22431
23634
  if (updated) await writeAccessKeys(updated);
22432
23635
  return updated ?? store$1;
22433
23636
  });
22434
- writeChain$3 = next.catch(() => {});
23637
+ writeChain$2 = next.catch(() => {});
22435
23638
  return next;
22436
23639
  }
22437
23640
 
@@ -22486,14 +23689,14 @@ const EMPTY_TIERS = {
22486
23689
  function tierRouting(config, tier) {
22487
23690
  return config.tiers[tier];
22488
23691
  }
22489
- function isObj$7(v) {
23692
+ function isObj$6(v) {
22490
23693
  return typeof v === "object" && v !== null && !Array.isArray(v);
22491
23694
  }
22492
23695
  function normalizeTiers(raw$1) {
22493
- if (!isObj$7(raw$1)) return { ...EMPTY_TIERS };
23696
+ if (!isObj$6(raw$1)) return { ...EMPTY_TIERS };
22494
23697
  if (raw$1.version !== TIERS_VERSION) throw new Error(`tiers.json version ${String(raw$1.version)} not supported (expected ${TIERS_VERSION})`);
22495
23698
  const tiers$1 = {};
22496
- if (isObj$7(raw$1.tiers)) for (const t of TIERS) {
23699
+ if (isObj$6(raw$1.tiers)) for (const t of TIERS) {
22497
23700
  const routing = normalizeRouting(raw$1.tiers[t]);
22498
23701
  if (routing && routing.target.kind !== "passthrough") tiers$1[t] = routing;
22499
23702
  }
@@ -22509,28 +23712,28 @@ async function readTiers() {
22509
23712
  async function writeTiers(config) {
22510
23713
  await atomicWrite(paths.tiers, `${JSON.stringify(config, null, 2)}\n`);
22511
23714
  }
22512
- let writeChain$2 = Promise.resolve();
23715
+ let writeChain$1 = Promise.resolve();
22513
23716
  /** Serialized read-modify-write — see model-registry.ts mutateModelRegistry. */
22514
23717
  function mutateTiers(fn) {
22515
- const next = writeChain$2.then(async () => {
23718
+ const next = writeChain$1.then(async () => {
22516
23719
  const config = await readTiers();
22517
23720
  const updated = fn(config);
22518
23721
  if (updated) await writeTiers(updated);
22519
23722
  return updated ?? config;
22520
23723
  });
22521
- writeChain$2 = next.catch(() => {});
23724
+ writeChain$1 = next.catch(() => {});
22522
23725
  return next;
22523
23726
  }
22524
23727
 
22525
23728
  //#endregion
22526
23729
  //#region src/daemon/api/keys.ts
22527
- function isObj$6(v) {
23730
+ function isObj$5(v) {
22528
23731
  return typeof v === "object" && v !== null && !Array.isArray(v);
22529
23732
  }
22530
23733
  async function jsonBody$3(c) {
22531
23734
  try {
22532
23735
  const body = await c.req.json();
22533
- return isObj$6(body) ? body : null;
23736
+ return isObj$5(body) ? body : null;
22534
23737
  } catch {
22535
23738
  return null;
22536
23739
  }
@@ -22705,7 +23908,7 @@ const DEFAULT_MASKING_CONFIG = {
22705
23908
  fakes: {},
22706
23909
  custom: []
22707
23910
  };
22708
- function isObj$5(v) {
23911
+ function isObj$4(v) {
22709
23912
  return typeof v === "object" && v !== null && !Array.isArray(v);
22710
23913
  }
22711
23914
  /** Compile a stored custom rule into a runnable one. Returns null on a bad
@@ -22730,11 +23933,11 @@ function compileCustomRule(c) {
22730
23933
  }
22731
23934
  }
22732
23935
  function normalizeMaskingConfig(raw$1) {
22733
- if (!isObj$5(raw$1) || raw$1.version !== MASKING_VERSION) return structuredCloneDefault();
23936
+ if (!isObj$4(raw$1) || raw$1.version !== MASKING_VERSION) return structuredCloneDefault();
22734
23937
  const custom = [];
22735
23938
  const customIds = new Set();
22736
23939
  if (Array.isArray(raw$1.custom)) for (const c of raw$1.custom) {
22737
- if (!isObj$5(c)) continue;
23940
+ if (!isObj$4(c)) continue;
22738
23941
  const cfg = {
22739
23942
  id: String(c.id ?? ""),
22740
23943
  label: String(c.label ?? c.id ?? ""),
@@ -22750,13 +23953,13 @@ function normalizeMaskingConfig(raw$1) {
22750
23953
  }
22751
23954
  const knownIds = new Set([...BUILTIN_IDS, ...customIds]);
22752
23955
  const providers = {};
22753
- if (isObj$5(raw$1.providers)) for (const [providerId, ids] of Object.entries(raw$1.providers)) {
23956
+ if (isObj$4(raw$1.providers)) for (const [providerId, ids] of Object.entries(raw$1.providers)) {
22754
23957
  if (!Array.isArray(ids)) continue;
22755
23958
  const valid = [...new Set(ids.filter((x) => typeof x === "string" && knownIds.has(x)))];
22756
23959
  if (valid.length > 0) providers[providerId] = valid;
22757
23960
  }
22758
23961
  const fakes = {};
22759
- if (isObj$5(raw$1.fakes)) {
23962
+ if (isObj$4(raw$1.fakes)) {
22760
23963
  for (const [id, fake] of Object.entries(raw$1.fakes)) if (knownIds.has(id) && typeof fake === "string" && fake.length > 0) fakes[id] = fake;
22761
23964
  }
22762
23965
  return {
@@ -22785,17 +23988,17 @@ async function readMaskingConfig() {
22785
23988
  async function writeMaskingConfig(cfg) {
22786
23989
  await atomicWrite(paths.masking, `${JSON.stringify(cfg, null, 2)}\n`);
22787
23990
  }
22788
- let writeChain$1 = Promise.resolve();
23991
+ let writeChain = Promise.resolve();
22789
23992
  /** Serialized read-modify-write so concurrent edits can't clobber each other
22790
23993
  * (mirrors core/secrets.ts). */
22791
23994
  function mutateMaskingConfig(fn) {
22792
- const next = writeChain$1.then(async () => {
23995
+ const next = writeChain.then(async () => {
22793
23996
  const cfg = await readMaskingConfig();
22794
23997
  const updated = normalizeMaskingConfig(fn(cfg));
22795
23998
  await writeMaskingConfig(updated);
22796
23999
  return updated;
22797
24000
  });
22798
- writeChain$1 = next.catch(() => {});
24001
+ writeChain = next.catch(() => {});
22799
24002
  return next;
22800
24003
  }
22801
24004
  /** The full runnable rule set for a config: built-ins followed by compiled
@@ -22960,245 +24163,6 @@ function maskCredentials(text$1, rules) {
22960
24163
  };
22961
24164
  }
22962
24165
 
22963
- //#endregion
22964
- //#region src/core/model-registry.ts
22965
- const MODEL_REGISTRY_VERSION = 3;
22966
- /** OpenRouter Fusion caps its panel at 8 models; mirror that. */
22967
- const MAX_FUSION_PANEL = 8;
22968
- const EMPTY_REGISTRY = {
22969
- version: MODEL_REGISTRY_VERSION,
22970
- providers: [],
22971
- models: [],
22972
- combos: []
22973
- };
22974
- const MODEL_APIS$1 = [
22975
- "anthropic-messages",
22976
- "openai-chat",
22977
- "openai-responses"
22978
- ];
22979
- const MODALITIES$1 = [
22980
- "text",
22981
- "audio",
22982
- "image",
22983
- "video",
22984
- "pdf"
22985
- ];
22986
- const REASONING_EFFORTS$1 = [
22987
- "minimal",
22988
- "low",
22989
- "medium",
22990
- "high",
22991
- "xhigh"
22992
- ];
22993
- function findProvider(reg, id) {
22994
- return reg.providers.find((p) => p.id === id);
22995
- }
22996
- /** Look up a model by id, optionally scoped to a provider.
22997
- *
22998
- * Same model id can exist under multiple providers (e.g. a Xiangxin
22999
- * model harvested under `hermes/...` and also added by hand under a
23000
- * custom `og-text` provider). When `providerId` is supplied we match
23001
- * the exact pair; otherwise we fall back to the first matching id so
23002
- * legacy callers (and pre-providerId routing-policy entries) still
23003
- * resolve to *something* instead of breaking. */
23004
- function findModel(reg, id, providerId) {
23005
- if (providerId !== void 0) return reg.models.find((m) => m.id === id && m.providerId === providerId);
23006
- return reg.models.find((m) => m.id === id);
23007
- }
23008
- /** A model's effective wire format: its own override, else its provider's. */
23009
- function resolveApi(reg, model) {
23010
- return model.api ?? findProvider(reg, model.providerId)?.api;
23011
- }
23012
- function findCombo(reg, id) {
23013
- return reg.combos.find((c) => c.id === id);
23014
- }
23015
- function isObj$4(v) {
23016
- return typeof v === "object" && v !== null && !Array.isArray(v);
23017
- }
23018
- function normalizeAuth(raw$1) {
23019
- if (!isObj$4(raw$1)) return void 0;
23020
- if (raw$1.kind === "passthrough") return { kind: "passthrough" };
23021
- if (raw$1.kind === "agent-oauth" && (raw$1.agent === "claude-code" || raw$1.agent === "codex")) return {
23022
- kind: "agent-oauth",
23023
- agent: raw$1.agent
23024
- };
23025
- if (raw$1.kind === "bearer" && typeof raw$1.valueRef === "string") return {
23026
- kind: "bearer",
23027
- valueRef: raw$1.valueRef
23028
- };
23029
- if (raw$1.kind === "api-key" && typeof raw$1.header === "string" && raw$1.header !== "" && typeof raw$1.valueRef === "string") return {
23030
- kind: "api-key",
23031
- header: raw$1.header,
23032
- valueRef: raw$1.valueRef
23033
- };
23034
- return void 0;
23035
- }
23036
- function normalizeProvider(raw$1) {
23037
- if (!isObj$4(raw$1)) return void 0;
23038
- const { id, label, baseUrl, api: api$1, origin, seededFrom, reasoningEffort } = raw$1;
23039
- if (typeof id !== "string" || id === "") return void 0;
23040
- if (typeof baseUrl !== "string" || baseUrl === "") return void 0;
23041
- if (!MODEL_APIS$1.includes(api$1)) return void 0;
23042
- const auth = normalizeAuth(raw$1.auth);
23043
- if (!auth) return void 0;
23044
- return {
23045
- id,
23046
- label: typeof label === "string" ? label : id,
23047
- baseUrl,
23048
- api: api$1,
23049
- auth,
23050
- origin: origin === "manual" ? "manual" : "seeded",
23051
- ...typeof seededFrom === "string" ? { seededFrom } : {},
23052
- ...REASONING_EFFORTS$1.includes(reasoningEffort) ? { reasoningEffort } : {}
23053
- };
23054
- }
23055
- function normalizeCost(raw$1) {
23056
- if (!isObj$4(raw$1)) return void 0;
23057
- const { input, output, cacheRead, cacheWrite } = raw$1;
23058
- if (typeof input !== "number" || typeof output !== "number") return void 0;
23059
- return {
23060
- input,
23061
- output,
23062
- ...typeof cacheRead === "number" ? { cacheRead } : {},
23063
- ...typeof cacheWrite === "number" ? { cacheWrite } : {}
23064
- };
23065
- }
23066
- function normalizeModel(raw$1) {
23067
- if (!isObj$4(raw$1)) return void 0;
23068
- const { id, providerId, label, api: api$1, origin, contextWindow, maxTokens } = raw$1;
23069
- if (typeof id !== "string" || id === "") return void 0;
23070
- if (typeof providerId !== "string" || providerId === "") return void 0;
23071
- const input = Array.isArray(raw$1.input) ? raw$1.input.filter((m) => MODALITIES$1.includes(m)) : [];
23072
- const cost = normalizeCost(raw$1.cost);
23073
- return {
23074
- id,
23075
- providerId,
23076
- label: typeof label === "string" ? label : id,
23077
- ...MODEL_APIS$1.includes(api$1) ? { api: api$1 } : {},
23078
- input: input.length > 0 ? input : ["text"],
23079
- ...typeof contextWindow === "number" ? { contextWindow } : {},
23080
- ...typeof maxTokens === "number" ? { maxTokens } : {},
23081
- ...cost ? { cost } : {},
23082
- origin: origin === "manual" ? "manual" : "seeded"
23083
- };
23084
- }
23085
- function normalizeFusionEndpoint(raw$1) {
23086
- if (!isObj$4(raw$1)) return void 0;
23087
- if (typeof raw$1.modelId !== "string" || raw$1.modelId === "") return void 0;
23088
- return {
23089
- modelId: raw$1.modelId,
23090
- ...typeof raw$1.providerId === "string" && raw$1.providerId !== "" ? { providerId: raw$1.providerId } : {}
23091
- };
23092
- }
23093
- function normalizeWebSearch(raw$1) {
23094
- if (!isObj$4(raw$1)) return void 0;
23095
- return typeof raw$1.providerId === "string" && raw$1.providerId !== "" ? { providerId: raw$1.providerId } : {};
23096
- }
23097
- /** A fusion panel member: the primary model, its per-member failover rules
23098
- * (`switchOn` — token/USD caps + error), and the `fallback` model they switch
23099
- * to. `normalizeMember` parses the {modelId, providerId, switchOn} part. */
23100
- function normalizeFusionMember(raw$1) {
23101
- const base = normalizeMember(raw$1);
23102
- if (!base) return void 0;
23103
- const fallback = isObj$4(raw$1) ? normalizeFusionEndpoint(raw$1.fallback) : void 0;
23104
- return {
23105
- modelId: base.modelId,
23106
- ...base.providerId ? { providerId: base.providerId } : {},
23107
- ...base.switchOn ? { switchOn: base.switchOn } : {},
23108
- ...fallback ? { fallback } : {}
23109
- };
23110
- }
23111
- /** Lift a legacy failover-combo's combo-level vision/web_search capabilities to
23112
- * the fusion's combo-level vision/web_search, so they survive the move from
23113
- * failover chains to fusion. */
23114
- function legacyCaps(rawCaps) {
23115
- const caps = normalizeCapabilities(rawCaps);
23116
- const vision = caps?.vision?.via === "companion" ? {
23117
- modelId: caps.vision.modelId,
23118
- ...caps.vision.providerId ? { providerId: caps.vision.providerId } : {}
23119
- } : void 0;
23120
- const webSearch = caps?.web_search?.via === "local" ? caps.web_search.providerId ? { providerId: caps.web_search.providerId } : {} : void 0;
23121
- return {
23122
- ...vision ? { vision } : {},
23123
- ...webSearch ? { webSearch } : {}
23124
- };
23125
- }
23126
- /** A fusion combination model. Accepts the current `panel` shape and migrates
23127
- * the legacy failover-combo shape (`members` + `capabilities`) forward: each
23128
- * member becomes a panel member, and the old combo-level vision/web_search
23129
- * capabilities become the combo-level `vision`/`webSearch`. Dropped when it has
23130
- * no id or no resolvable panel member. */
23131
- function normalizeCombo(raw$1) {
23132
- if (!isObj$4(raw$1)) return void 0;
23133
- const { id, label } = raw$1;
23134
- if (typeof id !== "string" || id === "") return void 0;
23135
- let panel;
23136
- let migrated = {};
23137
- if (Array.isArray(raw$1.panel)) panel = raw$1.panel.map(normalizeFusionMember).filter((m) => m != null);
23138
- else if (Array.isArray(raw$1.members)) {
23139
- migrated = legacyCaps(raw$1.capabilities);
23140
- panel = raw$1.members.map(normalizeMember).filter((m) => m != null).map((m) => ({
23141
- modelId: m.modelId,
23142
- ...m.providerId ? { providerId: m.providerId } : {},
23143
- ...m.switchOn ? { switchOn: m.switchOn } : {}
23144
- }));
23145
- } else panel = [];
23146
- if (panel.length === 0) return void 0;
23147
- const vision = normalizeFusionEndpoint(raw$1.vision) ?? migrated.vision;
23148
- const webSearch = normalizeWebSearch(raw$1.webSearch) ?? migrated.webSearch;
23149
- const judge = normalizeFusionEndpoint(raw$1.judge);
23150
- const synthesizer = normalizeFusionEndpoint(raw$1.synthesizer);
23151
- const cheapModel = normalizeFusionEndpoint(raw$1.cheapModel);
23152
- return {
23153
- id,
23154
- label: typeof label === "string" && label !== "" ? label : id,
23155
- panel: panel.slice(0, MAX_FUSION_PANEL),
23156
- ...vision ? { vision } : {},
23157
- ...webSearch ? { webSearch } : {},
23158
- ...judge ? { judge } : {},
23159
- ...synthesizer ? { synthesizer } : {},
23160
- ...cheapModel ? { cheapModel } : {},
23161
- origin: "manual"
23162
- };
23163
- }
23164
- /** Coerce parsed JSON into a valid registry — malformed entries are dropped,
23165
- * a hand-edited file with one bad entry never wipes the rest. v1 (no `combos`)
23166
- * and v2 (failover-style combos) are accepted and migrated forward by
23167
- * `normalizeCombo` — v2 combos' `members`/`capabilities` become a fusion
23168
- * `panel`. Throws only on an unsupported version, so a future format is never
23169
- * silently downgraded. */
23170
- function normalizeModelRegistry(raw$1) {
23171
- if (!isObj$4(raw$1)) return { ...EMPTY_REGISTRY };
23172
- if (raw$1.version !== 1 && raw$1.version !== 2 && raw$1.version !== MODEL_REGISTRY_VERSION) throw new Error(`models.json version ${String(raw$1.version)} not supported (expected ${MODEL_REGISTRY_VERSION})`);
23173
- const providers = Array.isArray(raw$1.providers) ? raw$1.providers.map(normalizeProvider).filter((p) => p != null) : [];
23174
- const models = Array.isArray(raw$1.models) ? raw$1.models.map(normalizeModel).filter((m) => m != null) : [];
23175
- const combos = Array.isArray(raw$1.combos) ? raw$1.combos.map(normalizeCombo).filter((c) => c != null) : [];
23176
- return {
23177
- version: MODEL_REGISTRY_VERSION,
23178
- providers,
23179
- models,
23180
- combos
23181
- };
23182
- }
23183
- async function readModelRegistry() {
23184
- if (!await fileExists(paths.models)) return { ...EMPTY_REGISTRY };
23185
- return normalizeModelRegistry(JSON.parse(await readFile(paths.models, "utf8")));
23186
- }
23187
- async function writeModelRegistry(reg) {
23188
- await atomicWrite(paths.models, `${JSON.stringify(reg, null, 2)}\n`);
23189
- }
23190
- let writeChain = Promise.resolve();
23191
- function mutateModelRegistry(fn) {
23192
- const next = writeChain.then(async () => {
23193
- const reg = await readModelRegistry();
23194
- const updated = fn(reg);
23195
- if (updated) await writeModelRegistry(updated);
23196
- return updated ?? reg;
23197
- });
23198
- writeChain = next.catch(() => {});
23199
- return next;
23200
- }
23201
-
23202
24166
  //#endregion
23203
24167
  //#region src/daemon/routing/load.ts
23204
24168
  let registry = EMPTY_REGISTRY;
@@ -23556,13 +24520,6 @@ const MODALITIES = [
23556
24520
  "video",
23557
24521
  "pdf"
23558
24522
  ];
23559
- const REASONING_EFFORTS = [
23560
- "minimal",
23561
- "low",
23562
- "medium",
23563
- "high",
23564
- "xhigh"
23565
- ];
23566
24523
  const ONE_DAY = 24 * 36e5;
23567
24524
  function isObj$3(v) {
23568
24525
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -23575,6 +24532,16 @@ async function jsonBody$2(c) {
23575
24532
  return null;
23576
24533
  }
23577
24534
  }
24535
+ function normalizeReasoningEffort(raw$1) {
24536
+ if (typeof raw$1 !== "string") return void 0;
24537
+ const value = raw$1.trim().toLowerCase();
24538
+ return REASONING_EFFORTS.includes(value) ? value : void 0;
24539
+ }
24540
+ function normalizeGenerationPath(raw$1) {
24541
+ if (typeof raw$1 !== "string") return void 0;
24542
+ const value = raw$1.trim().toLowerCase();
24543
+ return GENERATION_PATH_MODES.includes(value) ? value : void 0;
24544
+ }
23578
24545
  async function handleGetRegistry(c) {
23579
24546
  const reg = await readModelRegistry();
23580
24547
  const secrets$1 = await readSecrets();
@@ -23613,13 +24580,24 @@ async function handlePostProvider(c) {
23613
24580
  if (!providedId && !name) return c.json({ error: "provider name required" }, 400);
23614
24581
  if (!baseUrl) return c.json({ error: "baseUrl required" }, 400);
23615
24582
  if (!MODEL_APIS.includes(api$1)) return c.json({ error: `api must be one of ${MODEL_APIS.join(", ")}` }, 400);
24583
+ const reasoningEffort = normalizeReasoningEffort(body.reasoningEffort);
24584
+ if (body.reasoningEffort != null && !reasoningEffort) return c.json({ error: `reasoningEffort must be one of ${REASONING_EFFORTS.join(", ")}` }, 400);
24585
+ const generationPath = normalizeGenerationPath(body.generationPath);
24586
+ if (body.generationPath != null && !generationPath) return c.json({ error: `generationPath must be one of ${GENERATION_PATH_MODES.join(", ")}` }, 400);
23616
24587
  const reg0 = await readModelRegistry();
23617
24588
  const id = providedId || generateProviderId(name, reg0.providers.map((p) => p.id));
23618
24589
  const label = name || reg0.providers.find((p) => p.id === id)?.label || id;
23619
24590
  const authKind = body.authKind;
23620
24591
  let auth;
23621
24592
  if (authKind === "passthrough") auth = { kind: "passthrough" };
23622
- else if (authKind === "bearer" || authKind === "api-key") {
24593
+ else if (authKind === "agent-oauth") {
24594
+ const agent = body.agent;
24595
+ if (agent !== "claude-code" && agent !== "codex") return c.json({ error: "agent-oauth requires agent \"claude-code\" or \"codex\"" }, 400);
24596
+ auth = {
24597
+ kind: "agent-oauth",
24598
+ agent
24599
+ };
24600
+ } else if (authKind === "bearer" || authKind === "api-key") {
23623
24601
  const valueRef = `provider:${id}`;
23624
24602
  if (typeof body.apiKey === "string" && body.apiKey !== "") await setSecret(valueRef, body.apiKey);
23625
24603
  if (authKind === "bearer") auth = {
@@ -23635,8 +24613,7 @@ async function handlePostProvider(c) {
23635
24613
  valueRef
23636
24614
  };
23637
24615
  }
23638
- } else return c.json({ error: "authKind must be passthrough, bearer, or api-key" }, 400);
23639
- const reasoningEffort = REASONING_EFFORTS.includes(body.reasoningEffort) ? body.reasoningEffort : void 0;
24616
+ } else return c.json({ error: "authKind must be passthrough, agent-oauth, bearer, or api-key" }, 400);
23640
24617
  const provider = {
23641
24618
  id,
23642
24619
  label,
@@ -23644,6 +24621,7 @@ async function handlePostProvider(c) {
23644
24621
  api: api$1,
23645
24622
  auth,
23646
24623
  origin: "manual",
24624
+ ...generationPath ? { generationPath } : {},
23647
24625
  ...reasoningEffort ? { reasoningEffort } : {}
23648
24626
  };
23649
24627
  const reg = await mutateModelRegistry((r) => ({
@@ -23664,15 +24642,15 @@ async function handlePostProviderEffort(c) {
23664
24642
  const id = typeof body.id === "string" ? body.id.trim() : "";
23665
24643
  if (!id) return c.json({ error: "provider id required" }, 400);
23666
24644
  const raw$1 = body.reasoningEffort;
23667
- if (raw$1 != null && !REASONING_EFFORTS.includes(raw$1)) return c.json({ error: `reasoningEffort must be one of ${REASONING_EFFORTS.join(", ")} or null` }, 400);
23668
- const effort = raw$1 == null ? void 0 : raw$1;
24645
+ const effort = raw$1 == null ? void 0 : normalizeReasoningEffort(raw$1);
24646
+ if (raw$1 != null && !effort) return c.json({ error: `reasoningEffort must be one of ${REASONING_EFFORTS.join(", ")} or null` }, 400);
23669
24647
  const reg = await mutateModelRegistry((r) => ({
23670
24648
  ...r,
23671
24649
  providers: r.providers.map((p) => {
23672
24650
  if (p.id !== id) return p;
23673
24651
  const next = { ...p };
23674
24652
  if (effort) next.reasoningEffort = effort;
23675
- else delete next.reasoningEffort;
24653
+ else next.reasoningEffort = void 0;
23676
24654
  return next;
23677
24655
  })
23678
24656
  }));
@@ -23712,15 +24690,51 @@ function normalizeCostInput(raw$1) {
23712
24690
  ...typeof cacheWrite === "number" ? { cacheWrite } : {}
23713
24691
  };
23714
24692
  }
24693
+ function rewriteFusionEndpoint(endpoint, fromModelId, toModelId, providerId) {
24694
+ if (!endpoint) return void 0;
24695
+ if (endpoint.modelId !== fromModelId || endpoint.providerId !== providerId) return endpoint;
24696
+ return {
24697
+ ...endpoint,
24698
+ modelId: toModelId
24699
+ };
24700
+ }
24701
+ function rewriteFusionMember(member, fromModelId, toModelId, providerId) {
24702
+ const primary = rewriteFusionEndpoint(member, fromModelId, toModelId, providerId);
24703
+ const fallback = rewriteFusionEndpoint(member.fallback, fromModelId, toModelId, providerId);
24704
+ if (primary === member && fallback === member.fallback) return member;
24705
+ return {
24706
+ ...member,
24707
+ modelId: primary?.modelId ?? member.modelId,
24708
+ ...primary?.providerId ? { providerId: primary.providerId } : {},
24709
+ ...fallback ? { fallback } : {}
24710
+ };
24711
+ }
24712
+ function rewriteComboModelRefs(combo, fromModelId, toModelId, providerId) {
24713
+ const panel = combo.panel.map((m) => rewriteFusionMember(m, fromModelId, toModelId, providerId));
24714
+ const vision = rewriteFusionEndpoint(combo.vision, fromModelId, toModelId, providerId);
24715
+ const judge = rewriteFusionEndpoint(combo.judge, fromModelId, toModelId, providerId);
24716
+ const synthesizer = rewriteFusionEndpoint(combo.synthesizer, fromModelId, toModelId, providerId);
24717
+ if (panel.every((m, i) => m === combo.panel[i]) && vision === combo.vision && judge === combo.judge && synthesizer === combo.synthesizer) return combo;
24718
+ return {
24719
+ ...combo,
24720
+ panel,
24721
+ ...vision ? { vision } : {},
24722
+ ...judge ? { judge } : {},
24723
+ ...synthesizer ? { synthesizer } : {}
24724
+ };
24725
+ }
23715
24726
  async function handlePostModel(c) {
23716
24727
  const body = await jsonBody$2(c);
23717
24728
  if (!body) return c.json({ error: "malformed JSON body" }, 400);
23718
24729
  const id = typeof body.id === "string" ? body.id.trim() : "";
24730
+ const previousId = typeof body.previousId === "string" ? body.previousId.trim() : "";
23719
24731
  const providerId = typeof body.providerId === "string" ? body.providerId.trim() : "";
23720
24732
  if (!id) return c.json({ error: "model id required" }, 400);
23721
24733
  if (!providerId) return c.json({ error: "providerId required" }, 400);
23722
24734
  const reg0 = await readModelRegistry();
23723
24735
  if (!findProvider(reg0, providerId)) return c.json({ error: `unknown provider "${providerId}"` }, 400);
24736
+ const reasoningEffort = normalizeReasoningEffort(body.reasoningEffort);
24737
+ if (body.reasoningEffort != null && !reasoningEffort) return c.json({ error: `reasoningEffort must be one of ${REASONING_EFFORTS.join(", ")}` }, 400);
23724
24738
  const cost = normalizeCostInput(body.cost);
23725
24739
  const model = {
23726
24740
  id,
@@ -23731,11 +24745,19 @@ async function handlePostModel(c) {
23731
24745
  ...typeof body.contextWindow === "number" ? { contextWindow: body.contextWindow } : {},
23732
24746
  ...typeof body.maxTokens === "number" ? { maxTokens: body.maxTokens } : {},
23733
24747
  ...cost ? { cost } : {},
24748
+ ...reasoningEffort ? { reasoningEffort } : {},
23734
24749
  origin: "manual"
23735
24750
  };
24751
+ const isRename = previousId !== "" && previousId !== id;
23736
24752
  const reg = await mutateModelRegistry((r) => ({
23737
24753
  ...r,
23738
- models: [...r.models.filter((m) => !(m.id === id && m.providerId === providerId)), model]
24754
+ models: [...r.models.filter((m) => {
24755
+ if (m.providerId !== providerId) return true;
24756
+ if (m.id === id) return false;
24757
+ if (isRename && m.id === previousId) return false;
24758
+ return true;
24759
+ }), model],
24760
+ combos: isRename ? r.combos.map((combo) => rewriteComboModelRefs(combo, previousId, id, providerId)) : r.combos
23739
24761
  }));
23740
24762
  return c.json({
23741
24763
  ok: true,
@@ -24049,7 +25071,7 @@ async function handlePostSubagent(c) {
24049
25071
  if (typeof body.providerId === "string") {
24050
25072
  const pid = body.providerId.trim();
24051
25073
  if (pid) next.providerId = pid;
24052
- else delete next.providerId;
25074
+ else next.providerId = void 0;
24053
25075
  }
24054
25076
  if (typeof body.minMaxTokens === "number" && body.minMaxTokens >= 0) next.minMaxTokens = body.minMaxTokens;
24055
25077
  await mutateRoutingPolicy((p) => ({
@@ -24232,7 +25254,7 @@ async function handleDeleteCapability(c) {
24232
25254
  const capabilities = { ...prior.capabilities };
24233
25255
  delete capabilities[capabilityId];
24234
25256
  const next = { ...prior };
24235
- if (Object.keys(capabilities).length === 0) delete next.capabilities;
25257
+ if (Object.keys(capabilities).length === 0) next.capabilities = void 0;
24236
25258
  else next.capabilities = capabilities;
24237
25259
  return {
24238
25260
  ...p,
@@ -36079,8 +37101,8 @@ async function handlePostActiveToolProvider(c) {
36079
37101
  const active = { ...s.active };
36080
37102
  if (providerId === "") delete active[kind];
36081
37103
  else {
36082
- const exists$1 = s.providers.some((p) => p.id === providerId && p.kind === kind);
36083
- if (!exists$1) return void 0;
37104
+ const exists$2 = s.providers.some((p) => p.id === providerId && p.kind === kind);
37105
+ if (!exists$2) return void 0;
36084
37106
  active[kind] = providerId;
36085
37107
  }
36086
37108
  return {
@@ -38336,6 +39358,24 @@ function collectOutputBlocks(output) {
38336
39358
  });
38337
39359
  break;
38338
39360
  }
39361
+ case "local_shell_call": {
39362
+ blocks.push({
39363
+ type: "tool_use",
39364
+ id: it.call_id ?? it.id ?? "",
39365
+ name: "local_shell",
39366
+ input: it.action && typeof it.action === "object" ? it.action : {}
39367
+ });
39368
+ break;
39369
+ }
39370
+ case "custom_tool_call": {
39371
+ blocks.push({
39372
+ type: "tool_use",
39373
+ id: it.call_id ?? it.id ?? "",
39374
+ name: typeof it.name === "string" ? it.name : "",
39375
+ input: typeof it.input === "string" ? it.input : it.input ?? {}
39376
+ });
39377
+ break;
39378
+ }
38339
39379
  case "function_call":
38340
39380
  case "tool_call": {
38341
39381
  const name = typeof it.name === "string" ? it.name : it.function?.name ?? "";
@@ -38538,7 +39578,31 @@ function inputToMessages(input) {
38538
39578
  });
38539
39579
  continue;
38540
39580
  }
38541
- if (it.type === "function_call_output" || it.type === "tool_result") {
39581
+ if (it.type === "local_shell_call") {
39582
+ out.push({
39583
+ role: "assistant",
39584
+ content: [{
39585
+ type: "tool_use",
39586
+ id: it.call_id ?? it.id ?? "",
39587
+ name: "local_shell",
39588
+ input: it.action && typeof it.action === "object" ? it.action : {}
39589
+ }]
39590
+ });
39591
+ continue;
39592
+ }
39593
+ if (it.type === "custom_tool_call") {
39594
+ out.push({
39595
+ role: "assistant",
39596
+ content: [{
39597
+ type: "tool_use",
39598
+ id: it.call_id ?? it.id ?? "",
39599
+ name: it.name ?? "",
39600
+ input: tryParseArgs(it.input)
39601
+ }]
39602
+ });
39603
+ continue;
39604
+ }
39605
+ if (it.type === "function_call_output" || it.type === "tool_result" || it.type === "custom_tool_call_output" || it.type === "local_shell_call_output") {
38542
39606
  out.push({
38543
39607
  role: "tool",
38544
39608
  tool_call_id: it.call_id ?? it.id ?? "",
@@ -38824,7 +39888,7 @@ Use the tools provided by the harness to accomplish the user's task. Persist unt
38824
39888
  */
38825
39889
  function adaptForCodexBackend(body, reasoningEffort) {
38826
39890
  body.store = false;
38827
- delete body.max_output_tokens;
39891
+ body.max_output_tokens = void 0;
38828
39892
  const effort = EFFORT_TO_CODEX[reasoningEffort ?? "medium"];
38829
39893
  const existingReasoning = typeof body.reasoning === "object" && body.reasoning !== null ? body.reasoning : {};
38830
39894
  body.reasoning = {
@@ -38853,6 +39917,21 @@ function adaptForCodexBackend(body, reasoningEffort) {
38853
39917
  }, ...input];
38854
39918
  body.instructions = CODEX_IDENTITY_INSTRUCTIONS;
38855
39919
  }
39920
+ /** Apply the public/OpenAI-compatible Responses reasoning knob without
39921
+ * clobbering an explicit client-supplied `reasoning.effort`. Unlike the
39922
+ * codex ChatGPT backend adapter, this does not clamp `xhigh`; third-party
39923
+ * Responses-compatible gateways may support it even when OpenAI's public API
39924
+ * does not. */
39925
+ function applyOpenAIResponsesReasoning(body, reasoningEffort) {
39926
+ if (!reasoningEffort) return false;
39927
+ const existing = typeof body.reasoning === "object" && body.reasoning !== null ? body.reasoning : {};
39928
+ if ("effort" in existing) return false;
39929
+ body.reasoning = {
39930
+ ...existing,
39931
+ effort: reasoningEffort
39932
+ };
39933
+ return true;
39934
+ }
38856
39935
  /** Anthropic's Messages thinking-budget tiers mapped from our shared
38857
39936
  * effort knob. Values mirror Claude Code's documented presets (low /
38858
39937
  * medium / high / "Ultrathink"); `minimal` falls back to the smallest
@@ -39033,6 +40112,64 @@ function stringifyToolArgs(input) {
39033
40112
  return "{}";
39034
40113
  }
39035
40114
  }
40115
+ const LOCAL_SHELL_TOOL = "local_shell";
40116
+ /** Function-tool JSON schema mirroring `local_shell`'s exec action — an argv
40117
+ * array plus the optional working-directory / timeout codex passes through. */
40118
+ const LOCAL_SHELL_SCHEMA = {
40119
+ type: "object",
40120
+ properties: {
40121
+ command: {
40122
+ type: "array",
40123
+ items: { type: "string" },
40124
+ description: "The command to run as an argv array, e.g. [\"bash\",\"-lc\",\"ls -la\"]."
40125
+ },
40126
+ workdir: {
40127
+ type: "string",
40128
+ description: "Working directory for the command."
40129
+ },
40130
+ timeout_ms: {
40131
+ type: "number",
40132
+ description: "Timeout in milliseconds."
40133
+ }
40134
+ },
40135
+ required: ["command"]
40136
+ };
40137
+ /** A `local_shell_call` action → the function-call input we hand the chat
40138
+ * backend. Keeps `command` plus whatever optional knobs were present. */
40139
+ function shellActionToInput(action) {
40140
+ const a = asObject(action);
40141
+ const input = {};
40142
+ if (Array.isArray(a.command) || typeof a.command === "string") input.command = a.command;
40143
+ const workdir = a.workdir ?? a.working_directory;
40144
+ if (typeof workdir === "string") input.workdir = workdir;
40145
+ if (typeof a.timeout_ms === "number") input.timeout_ms = a.timeout_ms;
40146
+ return input;
40147
+ }
40148
+ /** The chat backend's function-call input → a `local_shell_call` exec action,
40149
+ * the shape codex consumes. Tolerates a `command` given as a string. */
40150
+ function inputToShellAction(input) {
40151
+ const i = typeof input === "string" ? safeJson(input) : asObject(input);
40152
+ const command = Array.isArray(i.command) ? i.command : typeof i.command === "string" ? [
40153
+ "bash",
40154
+ "-lc",
40155
+ i.command
40156
+ ] : [];
40157
+ const action = {
40158
+ type: "exec",
40159
+ command
40160
+ };
40161
+ const workdir = i.workdir ?? i.working_directory;
40162
+ if (typeof workdir === "string") action.working_directory = workdir;
40163
+ if (typeof i.timeout_ms === "number") action.timeout_ms = i.timeout_ms;
40164
+ return action;
40165
+ }
40166
+ function safeJson(raw$1) {
40167
+ try {
40168
+ return asObject(JSON.parse(raw$1));
40169
+ } catch {
40170
+ return {};
40171
+ }
40172
+ }
39036
40173
 
39037
40174
  //#endregion
39038
40175
  //#region src/daemon/translate/from-anthropic.ts
@@ -39575,16 +40712,25 @@ function requestToIR(body) {
39575
40712
  });
39576
40713
  } else if (Array.isArray(b.input)) for (const raw$1 of b.input) {
39577
40714
  const it = asObject(raw$1);
39578
- if (it.type === "function_call") messages.push({
40715
+ if (it.type === "function_call" || it.type === "custom_tool_call") messages.push({
39579
40716
  role: "assistant",
39580
40717
  content: [{
39581
40718
  type: "tool_use",
39582
40719
  id: str(it.call_id) ?? str(it.id) ?? "",
39583
40720
  name: str(it.name) ?? "",
39584
- input: parseToolArgs(it.arguments)
40721
+ input: parseToolArgs(it.arguments ?? it.input)
40722
+ }]
40723
+ });
40724
+ else if (it.type === "local_shell_call") messages.push({
40725
+ role: "assistant",
40726
+ content: [{
40727
+ type: "tool_use",
40728
+ id: str(it.call_id) ?? str(it.id) ?? "",
40729
+ name: LOCAL_SHELL_TOOL,
40730
+ input: shellActionToInput(it.action)
39585
40731
  }]
39586
40732
  });
39587
- else if (it.type === "function_call_output" || it.type === "tool_result") messages.push({
40733
+ else if (it.type === "function_call_output" || it.type === "tool_result" || it.type === "custom_tool_call_output" || it.type === "local_shell_call_output") messages.push({
39588
40734
  role: "user",
39589
40735
  content: [{
39590
40736
  type: "tool_result",
@@ -39669,6 +40815,14 @@ function toolsToIR(tools) {
39669
40815
  const out = [];
39670
40816
  for (const raw$1 of tools) {
39671
40817
  const t = asObject(raw$1);
40818
+ if (t.type === "local_shell") {
40819
+ out.push({
40820
+ name: LOCAL_SHELL_TOOL,
40821
+ description: "Run a shell command on the user's machine and return its stdout/stderr. Provide the command as an argv array.",
40822
+ inputSchema: LOCAL_SHELL_SCHEMA
40823
+ });
40824
+ continue;
40825
+ }
39672
40826
  const fn = asObject(t.function);
39673
40827
  const name = str(t.name) ?? str(fn.name);
39674
40828
  if (!name) continue;
@@ -39997,6 +41151,13 @@ function responseFromIR(ir) {
39997
41151
  annotations: []
39998
41152
  }]
39999
41153
  });
41154
+ else if (b.type === "tool_use" && b.name === LOCAL_SHELL_TOOL) output.push({
41155
+ type: "local_shell_call",
41156
+ id: `lsh_${nanoid()}`,
41157
+ call_id: b.id || `call_${nanoid()}`,
41158
+ status: "completed",
41159
+ action: inputToShellAction(b.input)
41160
+ });
40000
41161
  else if (b.type === "tool_use") output.push({
40001
41162
  type: "function_call",
40002
41163
  id: `fc_${nanoid()}`,
@@ -40699,7 +41860,8 @@ var OpenAIResponsesWriter = class {
40699
41860
  return itemAdded + partAdded;
40700
41861
  }
40701
41862
  if (ev.block.type === "tool_use") {
40702
- const itemId = `fc_${nanoid()}`;
41863
+ const isShell = ev.block.name === LOCAL_SHELL_TOOL;
41864
+ const itemId = `${isShell ? "lsh" : "fc"}_${nanoid()}`;
40703
41865
  this.acc.set(ev.index, {
40704
41866
  block: {
40705
41867
  type: "tool_use",
@@ -40713,16 +41875,26 @@ var OpenAIResponsesWriter = class {
40713
41875
  contentIndex: 0
40714
41876
  });
40715
41877
  this.order.push(ev.index);
41878
+ const callId = ev.block.id || `call_${nanoid()}`;
40716
41879
  return frame("response.output_item.added", {
40717
41880
  type: "response.output_item.added",
40718
41881
  sequence_number: this.seq++,
40719
41882
  output_index: outputIndex,
40720
- item: {
41883
+ item: isShell ? {
41884
+ id: itemId,
41885
+ type: "local_shell_call",
41886
+ status: "in_progress",
41887
+ call_id: callId,
41888
+ action: {
41889
+ type: "exec",
41890
+ command: []
41891
+ }
41892
+ } : {
40721
41893
  id: itemId,
40722
41894
  type: "function_call",
40723
41895
  status: "in_progress",
40724
41896
  arguments: "",
40725
- call_id: ev.block.id || `call_${nanoid()}`,
41897
+ call_id: callId,
40726
41898
  name: ev.block.name
40727
41899
  }
40728
41900
  });
@@ -40762,6 +41934,7 @@ var OpenAIResponsesWriter = class {
40762
41934
  const entry = this.acc.get(ev.index);
40763
41935
  if (!entry) return "";
40764
41936
  entry.toolJson += ev.json;
41937
+ if (entry.block.type === "tool_use" && entry.block.name === LOCAL_SHELL_TOOL) return "";
40765
41938
  return frame("response.function_call_arguments.delta", {
40766
41939
  type: "response.function_call_arguments.delta",
40767
41940
  sequence_number: this.seq++,
@@ -40816,6 +41989,19 @@ var OpenAIResponsesWriter = class {
40816
41989
  }
40817
41990
  if (entry.block.type === "tool_use") {
40818
41991
  const argsStr = entry.toolJson || "";
41992
+ const callId = entry.block.id || `call_${nanoid()}`;
41993
+ if (entry.block.name === LOCAL_SHELL_TOOL) return frame("response.output_item.done", {
41994
+ type: "response.output_item.done",
41995
+ sequence_number: this.seq++,
41996
+ output_index: entry.outputIndex,
41997
+ item: {
41998
+ id: entry.itemId,
41999
+ type: "local_shell_call",
42000
+ status: "completed",
42001
+ call_id: callId,
42002
+ action: inputToShellAction(parseToolArgs(argsStr))
42003
+ }
42004
+ });
40819
42005
  const argsDone = frame("response.function_call_arguments.done", {
40820
42006
  type: "response.function_call_arguments.done",
40821
42007
  sequence_number: this.seq++,
@@ -40832,7 +42018,7 @@ var OpenAIResponsesWriter = class {
40832
42018
  type: "function_call",
40833
42019
  status: "completed",
40834
42020
  arguments: argsStr,
40835
- call_id: entry.block.id || `call_${nanoid()}`,
42021
+ call_id: callId,
40836
42022
  name: entry.block.name
40837
42023
  }
40838
42024
  });
@@ -40857,6 +42043,13 @@ var OpenAIResponsesWriter = class {
40857
42043
  annotations: []
40858
42044
  }]
40859
42045
  });
42046
+ else if (entry.block.type === "tool_use" && entry.block.name === LOCAL_SHELL_TOOL) output.push({
42047
+ id: entry.itemId,
42048
+ type: "local_shell_call",
42049
+ status: "completed",
42050
+ call_id: entry.block.id || `call_${nanoid()}`,
42051
+ action: inputToShellAction(parseToolArgs(entry.toolJson || ""))
42052
+ });
40860
42053
  else if (entry.block.type === "tool_use") output.push({
40861
42054
  id: entry.itemId,
40862
42055
  type: "function_call",
@@ -40990,21 +42183,138 @@ function translateRequest(from, to, body) {
40990
42183
  }
40991
42184
 
40992
42185
  //#endregion
40993
- //#region src/daemon/orchestrator/oauth/jwt.ts
40994
- /** The `exp` claim (epoch seconds) of a JWT, or undefined when the token is
40995
- * malformed or carries no numeric `exp`. */
40996
- function decodeJwtExp(token) {
40997
- const parts = token.split(".");
40998
- const payload = parts[1];
40999
- if (parts.length < 2 || !payload) return void 0;
42186
+ //#region src/daemon/orchestrator/oauth/lock.ts
42187
+ const STALE_MS = 3e4;
42188
+ const MAX_WAIT_MS = 5e3;
42189
+ const POLL_MS = 100;
42190
+ async function withFileLock(path$1, fn) {
42191
+ const held = await acquire(path$1);
41000
42192
  try {
41001
- const json = Buffer$1.from(payload, "base64url").toString("utf8");
41002
- const claims = JSON.parse(json);
41003
- return typeof claims.exp === "number" ? claims.exp : void 0;
41004
- } catch {
41005
- return void 0;
42193
+ return await fn();
42194
+ } finally {
42195
+ if (held) await unlink(path$1).catch(() => {});
41006
42196
  }
41007
42197
  }
42198
+ async function acquire(path$1) {
42199
+ const deadline = Date.now() + MAX_WAIT_MS;
42200
+ for (;;) try {
42201
+ const fh = await open(path$1, "wx");
42202
+ await fh.close();
42203
+ return true;
42204
+ } catch (err) {
42205
+ if (err.code !== "EEXIST") return false;
42206
+ try {
42207
+ const st = await stat(path$1);
42208
+ if (Date.now() - st.mtimeMs > STALE_MS) {
42209
+ await unlink(path$1).catch(() => {});
42210
+ continue;
42211
+ }
42212
+ } catch {
42213
+ continue;
42214
+ }
42215
+ if (Date.now() > deadline) return false;
42216
+ await new Promise((r) => setTimeout(r, POLL_MS));
42217
+ }
42218
+ }
42219
+
42220
+ //#endregion
42221
+ //#region src/daemon/orchestrator/oauth/claude-code.ts
42222
+ const TOKEN_URL$1 = "https://platform.claude.com/v1/oauth/token";
42223
+ const CLIENT_ID$1 = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
42224
+ const SCOPE = "user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
42225
+ const EXPIRY_BUFFER_MS$1 = 5 * 60 * 1e3;
42226
+ /** A JSON-file credential store (afw's own store path, and the test seam). */
42227
+ function fileStore(path$1) {
42228
+ return {
42229
+ label: `file ${path$1}`,
42230
+ async read() {
42231
+ try {
42232
+ return JSON.parse(await readFile(path$1, "utf8"));
42233
+ } catch {
42234
+ return void 0;
42235
+ }
42236
+ },
42237
+ async write(creds) {
42238
+ await atomicWrite(path$1, `${JSON.stringify(creds, null, 2)}\n`, { mode: 384 });
42239
+ }
42240
+ };
42241
+ }
42242
+ /** afw's own token store. Returns undefined until the user has run
42243
+ * `afw oauth login anthropic`. */
42244
+ async function pickStore() {
42245
+ return await fileExists(paths.oauth.claudeCode) ? fileStore(paths.oauth.claudeCode) : void 0;
42246
+ }
42247
+ /** Exchange a refresh token for a fresh access token. The rotated refresh
42248
+ * token (when the provider returns one) is in the result and must be
42249
+ * written back to the agent's store by the caller. */
42250
+ async function refreshClaudeToken(refreshToken) {
42251
+ const res = await fetch(TOKEN_URL$1, {
42252
+ method: "POST",
42253
+ headers: { "content-type": "application/json" },
42254
+ body: JSON.stringify({
42255
+ grant_type: "refresh_token",
42256
+ refresh_token: refreshToken,
42257
+ client_id: CLIENT_ID$1,
42258
+ scope: SCOPE
42259
+ })
42260
+ });
42261
+ if (!res.ok) throw new Error(`claude-code OAuth refresh failed: HTTP ${res.status}`);
42262
+ const j = await res.json();
42263
+ if (!j.access_token) throw new Error("claude-code OAuth refresh: response carried no access_token");
42264
+ return {
42265
+ accessToken: j.access_token,
42266
+ refreshToken: j.refresh_token ?? refreshToken,
42267
+ expiresAt: Date.now() + (j.expires_in ?? 3600) * 1e3
42268
+ };
42269
+ }
42270
+ /** Resolve a usable access token from `store`, refreshing + writing back the
42271
+ * rotated token when the stored one is within the expiry buffer. No
42272
+ * in-memory cache and no lock — the production entry point adds those; this
42273
+ * is the unit-test seam. */
42274
+ async function resolveClaudeToken(store$1) {
42275
+ const creds = await store$1.read();
42276
+ const oauth = creds?.claudeAiOauth;
42277
+ if (!creds || !oauth?.accessToken || !oauth.refreshToken) throw new Error(`claude-code OAuth credentials unavailable (${store$1.label})`);
42278
+ const expiresAt = typeof oauth.expiresAt === "number" ? oauth.expiresAt : 0;
42279
+ if (Date.now() < expiresAt - EXPIRY_BUFFER_MS$1) return {
42280
+ token: oauth.accessToken,
42281
+ expiresAt
42282
+ };
42283
+ const refreshed = await refreshClaudeToken(oauth.refreshToken);
42284
+ const updated = {
42285
+ ...creds,
42286
+ claudeAiOauth: {
42287
+ ...oauth,
42288
+ accessToken: refreshed.accessToken,
42289
+ refreshToken: refreshed.refreshToken,
42290
+ expiresAt: refreshed.expiresAt
42291
+ }
42292
+ };
42293
+ await store$1.write(updated);
42294
+ return {
42295
+ token: refreshed.accessToken,
42296
+ expiresAt: refreshed.expiresAt
42297
+ };
42298
+ }
42299
+ let cache$1;
42300
+ let inflight$1;
42301
+ /** The orchestrator entry point: a fresh access token, cached in memory and
42302
+ * refreshed at most once across concurrent callers. */
42303
+ async function getClaudeCodeToken() {
42304
+ if (cache$1 && Date.now() < cache$1.expiresAt - EXPIRY_BUFFER_MS$1) return { token: cache$1.token };
42305
+ if (inflight$1) return inflight$1;
42306
+ inflight$1 = (async () => {
42307
+ const store$1 = await pickStore();
42308
+ if (!store$1) throw new Error("claude-code OAuth credentials not found");
42309
+ const lockPath = join(paths.home, "oauth-claude-code.lock");
42310
+ const resolved = await withFileLock(lockPath, () => resolveClaudeToken(store$1));
42311
+ cache$1 = resolved;
42312
+ return resolved;
42313
+ })().finally(() => {
42314
+ inflight$1 = void 0;
42315
+ });
42316
+ return inflight$1;
42317
+ }
41008
42318
 
41009
42319
  //#endregion
41010
42320
  //#region src/daemon/orchestrator/oauth/codex.ts
@@ -41106,7 +42416,7 @@ async function getCodexToken() {
41106
42416
  if (inflight) return inflight;
41107
42417
  inflight = (async () => {
41108
42418
  const lockPath = join(paths.home, "oauth-codex.lock");
41109
- const resolved = await withFileLock(lockPath, () => resolveCodexToken(paths.agent.codex.auth));
42419
+ const resolved = await withFileLock(lockPath, () => resolveCodexToken(paths.oauth.codex));
41110
42420
  cache = resolved;
41111
42421
  return resolved;
41112
42422
  })().finally(() => {
@@ -41122,61 +42432,6 @@ function getAgentToken(agent) {
41122
42432
  return agent === "codex" ? getCodexToken() : getClaudeCodeToken();
41123
42433
  }
41124
42434
 
41125
- //#endregion
41126
- //#region src/daemon/orchestrator/quota.ts
41127
- /** Latest known quota usage per provider id — the binding (most-consumed)
41128
- * window, as a 0–100 percentage. Account-global, so it reflects all usage of
41129
- * the subscription, not just afw's routed calls. */
41130
- const snapshots = new Map();
41131
- /** Parse rate-limit headers into the highest used-percentage across every
41132
- * limit/remaining pair found. Handles both layouts seen in the wild:
41133
- * - Anthropic: `anthropic-ratelimit-<bucket>-{limit,remaining}`
41134
- * - OpenAI: `x-ratelimit-{limit,remaining}-<bucket>`
41135
- * Returns undefined when no usable pair is present. */
41136
- function usedPctFromHeaders(headers) {
41137
- const buckets = new Map();
41138
- const put = (bucket, field, raw$1) => {
41139
- const n = Number.parseFloat(raw$1);
41140
- if (!Number.isFinite(n)) return;
41141
- const b = buckets.get(bucket) ?? {};
41142
- b[field] = n;
41143
- buckets.set(bucket, b);
41144
- };
41145
- headers.forEach((value, key) => {
41146
- const k = key.toLowerCase();
41147
- let m = /^anthropic-ratelimit-(.+)-(limit|remaining)$/.exec(k);
41148
- if (m) {
41149
- put(`a:${m[1]}`, m[2], value);
41150
- return;
41151
- }
41152
- m = /^x-ratelimit-(limit|remaining)-(.+)$/.exec(k);
41153
- if (m) put(`o:${m[2]}`, m[1], value);
41154
- });
41155
- let maxUsed;
41156
- for (const { limit, remaining } of buckets.values()) {
41157
- if (limit === void 0 || remaining === void 0 || limit <= 0) continue;
41158
- const used = (limit - Math.max(0, remaining)) / limit * 100;
41159
- if (maxUsed === void 0 || used > maxUsed) maxUsed = used;
41160
- }
41161
- return maxUsed;
41162
- }
41163
- /** Record a routed upstream response's quota headers against its provider.
41164
- * No-op when the response carries no recognizable rate-limit headers. */
41165
- function recordQuotaHeaders(providerId, headers) {
41166
- const usedPct = usedPctFromHeaders(headers);
41167
- if (usedPct === void 0) return;
41168
- snapshots.set(providerId, {
41169
- usedPct,
41170
- at: Date.now()
41171
- });
41172
- }
41173
- /** The latest known quota-used percentage for a provider, or undefined when
41174
- * none of its responses have carried rate-limit headers yet (so a `quota-pct`
41175
- * rule can't fire on missing data — we never switch blind). */
41176
- function quotaUsedPct(providerId) {
41177
- return snapshots.get(providerId)?.usedPct;
41178
- }
41179
-
41180
42435
  //#endregion
41181
42436
  //#region src/daemon/orchestrator/exec.ts
41182
42437
  /** Does this path target the API's model-generation endpoint? Guards the
@@ -41196,12 +42451,16 @@ function isGenerationPath(api$1, path$1) {
41196
42451
  * hits the right path because it constructs it itself; when an off-agent
41197
42452
  * route (openclaw, hermes, …) sends through this provider via afw, we
41198
42453
  * rebuild the URL here and must match codex's convention. */
41199
- function generationUrl(baseUrl, api$1) {
42454
+ function generationUrl(baseUrl, api$1, opts = {}) {
41200
42455
  const base = baseUrl.replace(/\/+$/, "");
41201
42456
  const rest = api$1 === "anthropic-messages" ? "messages" : api$1 === "openai-chat" ? "chat/completions" : "responses";
42457
+ if (opts.generationPath === "direct") return `${base}/${rest}`;
41202
42458
  if (isCodexChatGptBackend(base)) return `${base}/${rest}`;
41203
42459
  return base.endsWith("/v1") ? `${base}/${rest}` : `${base}/v1/${rest}`;
41204
42460
  }
42461
+ function effectiveReasoningEffort(member) {
42462
+ return member.reasoningEffort ?? member.model.reasoningEffort ?? member.provider.reasoningEffort;
42463
+ }
41205
42464
  /** Rewrite the `model` field of a request body. All three wire formats carry
41206
42465
  * the target model in a top-level `model` string, so one rewrite covers all.
41207
42466
  * An unparseable body is forwarded unchanged. */
@@ -41267,16 +42526,20 @@ function withoutServerTools(body) {
41267
42526
  });
41268
42527
  if (kept.length === tools.length) return body;
41269
42528
  const next = { ...body };
41270
- if (kept.length === 0) delete next.tools;
41271
- else next.tools = kept;
42529
+ if (kept.length === 0) {
42530
+ const { tools: _discardedTools,...rest } = next;
42531
+ return rest;
42532
+ }
42533
+ next.tools = kept;
41272
42534
  return next;
41273
42535
  }
41274
42536
  /** Apply an auth spec to forwarded request headers. Passthrough auth
41275
42537
  * leaves the client's own headers intact; otherwise every auth header the
41276
42538
  * client may have sent is dropped before the configured one is injected.
41277
42539
  * `agent-oauth` providers inject a subscription token afw reads — and
41278
- * co-refreshes — from the owning agent's own credential store. `id` is
41279
- * used purely for log labelling (provider id or route key). */
42540
+ * co-refreshes — from afw's OWN OAuth store (~/.afw/oauth/), populated by
42541
+ * `afw oauth login`; afw never reads the agent's own credential store. `id`
42542
+ * is used purely for log labelling (provider id or route key). */
41280
42543
  async function applyAuth(headers, auth, id) {
41281
42544
  if (auth.kind === "passthrough") return;
41282
42545
  if (auth.kind === "agent-oauth") {
@@ -41330,8 +42593,8 @@ function stripClientAuth(headers) {
41330
42593
  headers.delete("x-api-key");
41331
42594
  headers.delete("api-key");
41332
42595
  }
41333
- const CODEX_CLIENT_UA = "codex_cli_rs/0.81.0 (afw; openafw.com)";
41334
- const CLAUDE_CODE_CLIENT_UA = "claude-cli/2.0.0 (afw; openafw.com)";
42596
+ const CODEX_CLIENT_UA = "codex_cli_rs/0.81.0 (afw; openguardrails.com)";
42597
+ const CLAUDE_CODE_CLIENT_UA = "claude-cli/2.0.0 (afw; openguardrails.com)";
41335
42598
  function encodeJson(value) {
41336
42599
  const bytes = new TextEncoder().encode(JSON.stringify(value));
41337
42600
  const out = new ArrayBuffer(bytes.byteLength);
@@ -41405,6 +42668,20 @@ function clampOutputBudgetBytes(body, api$1, model) {
41405
42668
  const obj = parsed;
41406
42669
  return clampOutputBudget(obj, api$1, model) ? encodeJson(obj) : body;
41407
42670
  }
42671
+ /** Byte-level variant for the same-protocol fast path. Injects a configured
42672
+ * OpenAI Responses reasoning effort without parsing the body elsewhere. */
42673
+ function applyOpenAIResponsesReasoningBytes(body, reasoningEffort) {
42674
+ if (!reasoningEffort) return body;
42675
+ let parsed;
42676
+ try {
42677
+ parsed = JSON.parse(new TextDecoder().decode(body));
42678
+ } catch {
42679
+ return body;
42680
+ }
42681
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return body;
42682
+ const obj = parsed;
42683
+ return applyOpenAIResponsesReasoning(obj, reasoningEffort) ? encodeJson(obj) : body;
42684
+ }
41408
42685
  /** Recognized upstream context-overflow error signatures (lower-cased). Covers
41409
42686
  * vLLM ("maximum context length is N tokens … reduce the length of the input"),
41410
42687
  * OpenAI-compatible servers (`context_length_exceeded`), and Anthropic. */
@@ -41462,6 +42739,70 @@ function safeRetryBudget(text$1, model) {
41462
42739
  function isOverflowStatus(status) {
41463
42740
  return status === 400 || status === 413 || status === 422;
41464
42741
  }
42742
+ const TRANSIENT_RETRY_DELAYS_MS = [250, 750];
42743
+ function isTransientUpstreamStatus(status) {
42744
+ return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
42745
+ }
42746
+ function retryDelayMs(attempt, res) {
42747
+ const retryAfter = res?.headers.get("retry-after");
42748
+ if (retryAfter) {
42749
+ const seconds = Number.parseFloat(retryAfter);
42750
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1e3, 5e3);
42751
+ const when = Date.parse(retryAfter);
42752
+ if (Number.isFinite(when)) return Math.min(Math.max(when - Date.now(), 0), 5e3);
42753
+ }
42754
+ return TRANSIENT_RETRY_DELAYS_MS[attempt] ?? TRANSIENT_RETRY_DELAYS_MS.at(-1);
42755
+ }
42756
+ function sleep$1(ms) {
42757
+ return new Promise((resolve) => setTimeout(resolve, ms));
42758
+ }
42759
+ async function fetchBufferedWithTransientRetries(build, label) {
42760
+ let lastErr;
42761
+ for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt++) {
42762
+ let built;
42763
+ try {
42764
+ built = await build();
42765
+ const res = await fetch(built.request);
42766
+ const text$1 = restoreText(await res.text(), built.restore);
42767
+ if (!isTransientUpstreamStatus(res.status) || attempt === TRANSIENT_RETRY_DELAYS_MS.length) return {
42768
+ res,
42769
+ text: text$1
42770
+ };
42771
+ const delay = retryDelayMs(attempt, res);
42772
+ logger.warn(`routing: transient HTTP ${res.status} from ${label}; retrying in ${delay}ms (${attempt + 1}/${TRANSIENT_RETRY_DELAYS_MS.length})`);
42773
+ await sleep$1(delay);
42774
+ } catch (err) {
42775
+ lastErr = err;
42776
+ if (attempt === TRANSIENT_RETRY_DELAYS_MS.length) throw err;
42777
+ const delay = retryDelayMs(attempt);
42778
+ logger.warn(`routing: transient upstream error from ${label}: ${err.message}; retrying in ${delay}ms (${attempt + 1}/${TRANSIENT_RETRY_DELAYS_MS.length})`);
42779
+ await sleep$1(delay);
42780
+ }
42781
+ }
42782
+ throw lastErr instanceof Error ? lastErr : new Error("upstream call failed");
42783
+ }
42784
+ async function fetchStreamWithTransientRetries(build, label) {
42785
+ let lastErr;
42786
+ for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt++) try {
42787
+ const built = await build();
42788
+ const res = await fetch(built.request);
42789
+ if (!isTransientUpstreamStatus(res.status) || attempt === TRANSIENT_RETRY_DELAYS_MS.length) return {
42790
+ res,
42791
+ restore: built.restore
42792
+ };
42793
+ await res.text().catch(() => "");
42794
+ const delay = retryDelayMs(attempt, res);
42795
+ logger.warn(`routing: transient HTTP ${res.status} from ${label} [stream]; retrying in ${delay}ms (${attempt + 1}/${TRANSIENT_RETRY_DELAYS_MS.length})`);
42796
+ await sleep$1(delay);
42797
+ } catch (err) {
42798
+ lastErr = err;
42799
+ if (attempt === TRANSIENT_RETRY_DELAYS_MS.length) throw err;
42800
+ const delay = retryDelayMs(attempt);
42801
+ logger.warn(`routing: transient upstream error from ${label} [stream]: ${err.message}; retrying in ${delay}ms (${attempt + 1}/${TRANSIENT_RETRY_DELAYS_MS.length})`);
42802
+ await sleep$1(delay);
42803
+ }
42804
+ throw lastErr instanceof Error ? lastErr : new Error("upstream call failed");
42805
+ }
41465
42806
  /** Build the upstream HTTP request for a routed member: translate the client
41466
42807
  * request into the member's wire format, force the model and the stream flag,
41467
42808
  * apply provider auth, mask credentials for the member's provider, and target
@@ -41474,8 +42815,10 @@ async function buildUpstreamRequest(member, clientApi, clientRequest, ctx, upstr
41474
42815
  model: member.model.id,
41475
42816
  stream
41476
42817
  };
41477
- if (member.api === "anthropic-messages" && member.provider.auth.kind === "agent-oauth" && member.provider.auth.agent === "claude-code") adaptForClaudeCodeOAuth(upstreamBody, member.provider.reasoningEffort);
41478
- if (member.api === "openai-responses" && isCodexChatGptBackend(member.provider.baseUrl)) adaptForCodexBackend(upstreamBody, member.provider.reasoningEffort);
42818
+ const reasoningEffort = effectiveReasoningEffort(member);
42819
+ if (member.api === "anthropic-messages" && member.provider.auth.kind === "agent-oauth" && member.provider.auth.agent === "claude-code") adaptForClaudeCodeOAuth(upstreamBody, reasoningEffort);
42820
+ if (member.api === "openai-responses" && isCodexChatGptBackend(member.provider.baseUrl)) adaptForCodexBackend(upstreamBody, reasoningEffort);
42821
+ else if (member.api === "openai-responses") applyOpenAIResponsesReasoning(upstreamBody, reasoningEffort);
41479
42822
  clampOutputBudget(upstreamBody, member.api, member.model);
41480
42823
  if (outputOverride != null) {
41481
42824
  const fields = outputTokenFields(member.api);
@@ -41520,17 +42863,14 @@ function restoreStream(body, restore) {
41520
42863
  async function execAttempt(member, clientApi, clientRequest, ctx) {
41521
42864
  const startedAtWall = Date.now();
41522
42865
  const t0 = performance.now();
41523
- const upstreamUrl = generationUrl(member.provider.baseUrl, member.api);
42866
+ const upstreamUrl = generationUrl(member.provider.baseUrl, member.api, { generationPath: member.provider.generationPath });
42867
+ const label = `${member.provider.id}/${member.model.id}`;
41524
42868
  try {
41525
- const { request, restore } = await buildUpstreamRequest(member, clientApi, clientRequest, ctx, upstreamUrl, false);
41526
- let res = await fetch(request);
41527
- let text$1 = restoreText(await res.text(), restore);
42869
+ let { res, text: text$1 } = await fetchBufferedWithTransientRetries(() => buildUpstreamRequest(member, clientApi, clientRequest, ctx, upstreamUrl, false), label);
41528
42870
  if (res.status >= 400 && isOverflowStatus(res.status)) {
41529
42871
  const budget = safeRetryBudget(text$1, member.model);
41530
42872
  if (budget != null) {
41531
- const retry = await buildUpstreamRequest(member, clientApi, clientRequest, ctx, upstreamUrl, false, budget);
41532
- const res2 = await fetch(retry.request);
41533
- const text2 = restoreText(await res2.text(), retry.restore);
42873
+ const { res: res2, text: text2 } = await fetchBufferedWithTransientRetries(() => buildUpstreamRequest(member, clientApi, clientRequest, ctx, upstreamUrl, false, budget), `${label} overflow-retry`);
41534
42874
  if (res2.status < 400) {
41535
42875
  logger.info(`routing: ${member.model.id} retried with max output ${budget} after context overflow`);
41536
42876
  res = res2;
@@ -41539,7 +42879,6 @@ async function execAttempt(member, clientApi, clientRequest, ctx) {
41539
42879
  }
41540
42880
  }
41541
42881
  const durMs = performance.now() - t0;
41542
- recordQuotaHeaders(member.provider.id, res.headers);
41543
42882
  if (res.status >= 400) return {
41544
42883
  ok: false,
41545
42884
  status: res.status,
@@ -41590,23 +42929,21 @@ async function execAttempt(member, clientApi, clientRequest, ctx) {
41590
42929
  async function execStream(member, clientApi, clientRequest, ctx) {
41591
42930
  const startedAtWall = Date.now();
41592
42931
  const t0 = performance.now();
41593
- const upstreamUrl = generationUrl(member.provider.baseUrl, member.api);
42932
+ const upstreamUrl = generationUrl(member.provider.baseUrl, member.api, { generationPath: member.provider.generationPath });
42933
+ const label = `${member.provider.id}/${member.model.id}`;
41594
42934
  try {
41595
- const { request, restore } = await buildUpstreamRequest(member, clientApi, clientRequest, ctx, upstreamUrl, true);
41596
- const res = await fetch(request);
41597
- recordQuotaHeaders(member.provider.id, res.headers);
42935
+ const { res, restore } = await fetchStreamWithTransientRetries(() => buildUpstreamRequest(member, clientApi, clientRequest, ctx, upstreamUrl, true), label);
41598
42936
  if (res.status >= 400 && isOverflowStatus(res.status)) {
41599
42937
  const text$1 = restoreText(await res.text(), restore);
41600
42938
  const budget = safeRetryBudget(text$1, member.model);
41601
42939
  if (budget != null) {
41602
- const retry = await buildUpstreamRequest(member, clientApi, clientRequest, ctx, upstreamUrl, true, budget);
41603
- const res2 = await fetch(retry.request);
42940
+ const { res: res2, restore: restore2 } = await fetchStreamWithTransientRetries(() => buildUpstreamRequest(member, clientApi, clientRequest, ctx, upstreamUrl, true, budget), `${label} overflow-retry`);
41604
42941
  if (res2.status < 400 && res2.body) {
41605
42942
  logger.info(`routing: ${member.model.id} retried with max output ${budget} after context overflow [stream]`);
41606
42943
  return {
41607
42944
  ok: true,
41608
42945
  status: res2.status,
41609
- body: restoreStream(res2.body, retry.restore),
42946
+ body: restoreStream(res2.body, restore2),
41610
42947
  durMs: performance.now() - t0,
41611
42948
  startedAtWall,
41612
42949
  upstreamUrl
@@ -42101,6 +43438,19 @@ function requestHasImageBlock(clientApi, clientRequest) {
42101
43438
  return false;
42102
43439
  }
42103
43440
 
43441
+ //#endregion
43442
+ //#region src/daemon/orchestrator/quota.ts
43443
+ /** Latest known quota usage per provider id — the binding (most-consumed)
43444
+ * window, as a 0–100 percentage. Account-global, so it reflects all usage of
43445
+ * the subscription, not just afw's routed calls. */
43446
+ const snapshots = new Map();
43447
+ /** The latest known quota-used percentage for a provider, or undefined when
43448
+ * none of its responses have carried rate-limit headers yet (so a `quota-pct`
43449
+ * rule can't fire on missing data — we never switch blind). */
43450
+ function quotaUsedPct(providerId) {
43451
+ return snapshots.get(providerId)?.usedPct;
43452
+ }
43453
+
42104
43454
  //#endregion
42105
43455
  //#region src/daemon/orchestrator/resolve.ts
42106
43456
  /** A route's decoder → the wire API afw can translate, if any. */
@@ -42139,10 +43489,13 @@ function resolveModelRef(modelId, providerId) {
42139
43489
  if (!provider) return void 0;
42140
43490
  const api$1 = resolveApi(reg, model);
42141
43491
  if (!api$1) return void 0;
43492
+ const resolvedProvider = withResolvableAuth(provider);
43493
+ const reasoningEffort = model.reasoningEffort ?? resolvedProvider.reasoningEffort;
42142
43494
  return {
42143
43495
  model,
42144
- provider: withResolvableAuth(provider),
42145
- api: api$1
43496
+ provider: resolvedProvider,
43497
+ api: api$1,
43498
+ ...reasoningEffort ? { reasoningEffort } : {}
42146
43499
  };
42147
43500
  }
42148
43501
  /** Resolve a routeKey against the live routing policy. */
@@ -42193,6 +43546,7 @@ function resolveRouteFrom(routing, decoder, label = "route") {
42193
43546
  model: only.model,
42194
43547
  provider: only.provider,
42195
43548
  api: only.api,
43549
+ ...only.reasoningEffort ? { reasoningEffort: only.reasoningEffort } : {},
42196
43550
  capabilities,
42197
43551
  configuredTarget: {
42198
43552
  kind: "model",
@@ -42253,7 +43607,8 @@ function resolveFusion(routeKey, combo, clientApi) {
42253
43607
  const firstRef = {
42254
43608
  model: firstPrimary.model,
42255
43609
  provider: firstPrimary.provider,
42256
- api: firstPrimary.api
43610
+ api: firstPrimary.api,
43611
+ ...firstPrimary.reasoningEffort ? { reasoningEffort: firstPrimary.reasoningEffort } : {}
42257
43612
  };
42258
43613
  let synthesizer = firstRef;
42259
43614
  if (combo.synthesizer) {
@@ -42646,6 +44001,12 @@ function synthOpenAIResponses(ir) {
42646
44001
  id: `msg_${nanoid()}`,
42647
44002
  text: b.text
42648
44003
  });
44004
+ else if (b.type === "tool_use" && b.name === LOCAL_SHELL_TOOL) items.push({
44005
+ kind: "local_shell_call",
44006
+ id: `lsh_${nanoid()}`,
44007
+ callId: b.id || `call_${nanoid()}`,
44008
+ action: inputToShellAction(b.input)
44009
+ });
42649
44010
  else if (b.type === "tool_use") items.push({
42650
44011
  kind: "function_call",
42651
44012
  id: `fc_${nanoid()}`,
@@ -42660,23 +44021,33 @@ function synthOpenAIResponses(ir) {
42660
44021
  };
42661
44022
  if (ir.usage.cacheRead != null) usage.input_tokens_details = { cached_tokens: ir.usage.cacheRead };
42662
44023
  const incomplete = ir.stopReason === "max_tokens";
42663
- const finishedOutput = () => items.map((it) => it.kind === "message" ? {
42664
- id: it.id,
42665
- type: "message",
42666
- status: "completed",
42667
- role: "assistant",
42668
- content: [{
42669
- type: "output_text",
42670
- text: it.text,
42671
- annotations: []
42672
- }]
42673
- } : {
42674
- id: it.id,
42675
- type: "function_call",
42676
- status: "completed",
42677
- arguments: it.argsStr,
42678
- call_id: it.callId,
42679
- name: it.name
44024
+ const finishedOutput = () => items.map((it) => {
44025
+ if (it.kind === "message") return {
44026
+ id: it.id,
44027
+ type: "message",
44028
+ status: "completed",
44029
+ role: "assistant",
44030
+ content: [{
44031
+ type: "output_text",
44032
+ text: it.text,
44033
+ annotations: []
44034
+ }]
44035
+ };
44036
+ if (it.kind === "local_shell_call") return {
44037
+ id: it.id,
44038
+ type: "local_shell_call",
44039
+ status: "completed",
44040
+ call_id: it.callId,
44041
+ action: it.action
44042
+ };
44043
+ return {
44044
+ id: it.id,
44045
+ type: "function_call",
44046
+ status: "completed",
44047
+ arguments: it.argsStr,
44048
+ call_id: it.callId,
44049
+ name: it.name
44050
+ };
42680
44051
  });
42681
44052
  const baseResponse = (status) => ({
42682
44053
  id: responseId,
@@ -42793,6 +44164,37 @@ function synthOpenAIResponses(ir) {
42793
44164
  }
42794
44165
  })
42795
44166
  });
44167
+ } else if (it.kind === "local_shell_call") {
44168
+ events.push({
44169
+ event: "response.output_item.added",
44170
+ data: JSON.stringify({
44171
+ type: "response.output_item.added",
44172
+ sequence_number: seq$6++,
44173
+ output_index: outputIndex,
44174
+ item: {
44175
+ id: it.id,
44176
+ type: "local_shell_call",
44177
+ status: "in_progress",
44178
+ call_id: it.callId,
44179
+ action: it.action
44180
+ }
44181
+ })
44182
+ });
44183
+ events.push({
44184
+ event: "response.output_item.done",
44185
+ data: JSON.stringify({
44186
+ type: "response.output_item.done",
44187
+ sequence_number: seq$6++,
44188
+ output_index: outputIndex,
44189
+ item: {
44190
+ id: it.id,
44191
+ type: "local_shell_call",
44192
+ status: "completed",
44193
+ call_id: it.callId,
44194
+ action: it.action
44195
+ }
44196
+ })
44197
+ });
42796
44198
  } else {
42797
44199
  events.push({
42798
44200
  event: "response.output_item.added",
@@ -42996,7 +44398,7 @@ async function runSearch(provider, query, count) {
42996
44398
  }
42997
44399
  async function readBackendSecret(ref) {
42998
44400
  try {
42999
- const { readSecrets: readSecrets$1 } = await import("../secrets-9JqUBHyw.js");
44401
+ const { readSecrets: readSecrets$1 } = await import("../secrets-DJCX60WS.js");
43000
44402
  const secrets$1 = await readSecrets$1();
43001
44403
  return getSecret(secrets$1, ref) ?? void 0;
43002
44404
  } catch {
@@ -43257,6 +44659,38 @@ function parseJsonObject(body) {
43257
44659
  } catch {}
43258
44660
  return void 0;
43259
44661
  }
44662
+ const SAME_PROTOCOL_TRANSIENT_RETRY_DELAYS_MS = [250, 750];
44663
+ function sameProtocolRetryDelayMs(attempt, res) {
44664
+ const retryAfter = res?.headers.get("retry-after");
44665
+ if (retryAfter) {
44666
+ const seconds = Number.parseFloat(retryAfter);
44667
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1e3, 5e3);
44668
+ const when = Date.parse(retryAfter);
44669
+ if (Number.isFinite(when)) return Math.min(Math.max(when - Date.now(), 0), 5e3);
44670
+ }
44671
+ return SAME_PROTOCOL_TRANSIENT_RETRY_DELAYS_MS[attempt] ?? SAME_PROTOCOL_TRANSIENT_RETRY_DELAYS_MS.at(-1);
44672
+ }
44673
+ function sameProtocolSleep(ms) {
44674
+ return new Promise((resolve) => setTimeout(resolve, ms));
44675
+ }
44676
+ async function fetchSameProtocolWithTransientRetries(build, label) {
44677
+ let lastErr;
44678
+ for (let attempt = 0; attempt <= SAME_PROTOCOL_TRANSIENT_RETRY_DELAYS_MS.length; attempt++) try {
44679
+ const res = await fetch(build());
44680
+ if (!isTransientUpstreamStatus(res.status) || attempt === SAME_PROTOCOL_TRANSIENT_RETRY_DELAYS_MS.length) return res;
44681
+ await res.text().catch(() => "");
44682
+ const delay = sameProtocolRetryDelayMs(attempt, res);
44683
+ logger.warn(`routing: transient HTTP ${res.status} from ${label} [same-protocol]; retrying in ${delay}ms (${attempt + 1}/${SAME_PROTOCOL_TRANSIENT_RETRY_DELAYS_MS.length})`);
44684
+ await sameProtocolSleep(delay);
44685
+ } catch (err) {
44686
+ lastErr = err;
44687
+ if (attempt === SAME_PROTOCOL_TRANSIENT_RETRY_DELAYS_MS.length) throw err;
44688
+ const delay = sameProtocolRetryDelayMs(attempt);
44689
+ logger.warn(`routing: transient upstream error from ${label} [same-protocol]: ${err.message}; retrying in ${delay}ms (${attempt + 1}/${SAME_PROTOCOL_TRANSIENT_RETRY_DELAYS_MS.length})`);
44690
+ await sameProtocolSleep(delay);
44691
+ }
44692
+ throw lastErr instanceof Error ? lastErr : new Error("upstream call failed");
44693
+ }
43260
44694
  /** Buffer a single cross-protocol model swap: one upstream call, the response
43261
44695
  * translated into the client's wire format, captured as one packet. When the
43262
44696
  * route carries a vision capability and the request has images the model
@@ -43268,7 +44702,8 @@ async function runBuffered(ctx, resolved, req) {
43268
44702
  model: resolved.model,
43269
44703
  provider: resolved.provider,
43270
44704
  api: resolved.api,
43271
- switchOn: []
44705
+ switchOn: [],
44706
+ ...resolved.reasoningEffort ? { reasoningEffort: resolved.reasoningEffort } : {}
43272
44707
  };
43273
44708
  const execCtx = {
43274
44709
  agent: ctx.agent,
@@ -43297,7 +44732,8 @@ async function runBuffered(ctx, resolved, req) {
43297
44732
  model: visionCompanion.ref.model,
43298
44733
  provider: visionCompanion.ref.provider,
43299
44734
  api: visionCompanion.ref.api,
43300
- switchOn: []
44735
+ switchOn: [],
44736
+ ...visionCompanion.ref.reasoningEffort ? { reasoningEffort: visionCompanion.ref.reasoningEffort } : {}
43301
44737
  };
43302
44738
  if (visionMemberToUse) {
43303
44739
  const pre = await preDescribeImages(resolved.clientApi, req, visionMemberToUse, execCtx);
@@ -43396,14 +44832,15 @@ function visionCompanionShim(resolved) {
43396
44832
  kind: "vision",
43397
44833
  model: cap.ref.model,
43398
44834
  provider: cap.ref.provider,
43399
- api: cap.ref.api
44835
+ api: cap.ref.api,
44836
+ ...cap.ref.reasoningEffort ? { reasoningEffort: cap.ref.reasoningEffort } : {}
43400
44837
  };
43401
44838
  }
43402
44839
  /** Render a single attempt as a client response, or an error JSON when it
43403
44840
  * failed. A `stream:true` client gets a synthesized SSE body (the buffered
43404
44841
  * path cannot true-stream — see synth-sse.ts). */
43405
44842
  function buildClientResponse(clientApi, winner, attempts, wantsStream) {
43406
- if (winner && winner.result.ok) {
44843
+ if (winner?.result.ok) {
43407
44844
  const { ir, json, status: status$1 } = winner.result;
43408
44845
  if (wantsStream) return new Response(synthesizeSse(clientApi, ir), {
43409
44846
  status: status$1,
@@ -43455,7 +44892,8 @@ async function runChain(ctx, resolved, req) {
43455
44892
  model: visionToolModel.model,
43456
44893
  provider: visionToolModel.provider,
43457
44894
  api: visionToolModel.api,
43458
- switchOn: []
44895
+ switchOn: [],
44896
+ ...visionToolModel.reasoningEffort ? { reasoningEffort: visionToolModel.reasoningEffort } : {}
43459
44897
  };
43460
44898
  const pre = await preDescribeImages(clientApi, req, visionMember, execCtx);
43461
44899
  for (const a of pre.attempts) attempts.push({
@@ -43721,7 +45159,8 @@ async function runJudge(judge, clientApi, req, answers, execCtx) {
43721
45159
  model: judge.model,
43722
45160
  provider: judge.provider,
43723
45161
  api: judge.api,
43724
- switchOn: []
45162
+ switchOn: [],
45163
+ ...judge.reasoningEffort ? { reasoningEffort: judge.reasoningEffort } : {}
43725
45164
  };
43726
45165
  const userText = lastUserText(clientApi, req);
43727
45166
  const judgeIR = {
@@ -43765,14 +45204,15 @@ async function runSynthesis(synth, clientApi, req, describedReq, answers, analys
43765
45204
  model: synth.model,
43766
45205
  provider: synth.provider,
43767
45206
  api: synth.api,
43768
- switchOn: []
45207
+ switchOn: [],
45208
+ ...synth.reasoningEffort ? { reasoningEffort: synth.reasoningEffort } : {}
43769
45209
  };
43770
45210
  const synthTextOnly = !(synth.model.input ?? []).includes("image");
43771
45211
  const base = synthTextOnly && describedReq ? describedReq : req;
43772
45212
  let body;
43773
45213
  try {
43774
45214
  const ir = parseRequestToIR(clientApi, base);
43775
- const deliberation = `${FUSION_SYNTH_GUIDANCE}\n\nPanel answers:\n${renderPanelAnswers(answers)}` + (analysis ? `\n\nJudge analysis:\n${analysis}` : "");
45215
+ const deliberation = `${FUSION_SYNTH_GUIDANCE}\n\nPanel answers:\n${renderPanelAnswers(answers)}${analysis ? `\n\nJudge analysis:\n${analysis}` : ""}`;
43776
45216
  const messages = mergeConsecutive([...ir.messages, {
43777
45217
  role: "user",
43778
45218
  content: [{
@@ -43845,7 +45285,8 @@ async function runFusion(ctx, resolved, req) {
43845
45285
  model: resolved.vision.model,
43846
45286
  provider: resolved.vision.provider,
43847
45287
  api: resolved.vision.api,
43848
- switchOn: []
45288
+ switchOn: [],
45289
+ ...resolved.vision.reasoningEffort ? { reasoningEffort: resolved.vision.reasoningEffort } : {}
43849
45290
  };
43850
45291
  const pre = await preDescribeImages(clientApi, req, visionMember, execCtx);
43851
45292
  for (const a of pre.attempts) attempts.push(a);
@@ -43899,7 +45340,8 @@ async function runStreamingSwap(ctx, resolved, req) {
43899
45340
  model: resolved.model,
43900
45341
  provider: resolved.provider,
43901
45342
  api: resolved.api,
43902
- switchOn: []
45343
+ switchOn: [],
45344
+ ...resolved.reasoningEffort ? { reasoningEffort: resolved.reasoningEffort } : {}
43903
45345
  };
43904
45346
  beginRequest();
43905
45347
  const attempt = await execStream(member, clientApi, req, {
@@ -43973,10 +45415,15 @@ async function runStreamingSwap(ctx, resolved, req) {
43973
45415
  }
43974
45416
  async function runSameProtocolSwap(ctx, resolved, reqBody) {
43975
45417
  const { model, provider } = resolved;
43976
- const upstreamUrl = generationUrl(provider.baseUrl, resolved.api);
45418
+ const upstreamUrl = generationUrl(provider.baseUrl, resolved.api, { generationPath: provider.generationPath });
43977
45419
  let body = rewriteModel(reqBody, model.id);
43978
45420
  if (!isAnthropicNative(provider)) body = stripAnthropicServerToolsFromBody(body);
43979
45421
  body = clampOutputBudgetBytes(body, resolved.api, model);
45422
+ if (resolved.api === "openai-responses") body = applyOpenAIResponsesReasoningBytes(body, effectiveReasoningEffort({
45423
+ ...resolved.reasoningEffort ? { reasoningEffort: resolved.reasoningEffort } : {},
45424
+ model,
45425
+ provider
45426
+ }));
43980
45427
  const masked = maskRequestBody(body, provider.id);
43981
45428
  if (masked) body = masked.body;
43982
45429
  const headers = filterRequestHeaders(ctx.reqHeaders, new URL(upstreamUrl).host);
@@ -43989,11 +45436,12 @@ async function runSameProtocolSwap(ctx, resolved, reqBody) {
43989
45436
  beginRequest();
43990
45437
  let upstreamRes;
43991
45438
  try {
43992
- upstreamRes = await fetch(new Request(upstreamUrl, {
45439
+ const label = `${provider.id}/${model.id}`;
45440
+ upstreamRes = await fetchSameProtocolWithTransientRetries(() => new Request(upstreamUrl, {
43993
45441
  method: "POST",
43994
- headers,
43995
- body
43996
- }));
45442
+ headers: new Headers(headers),
45443
+ body: body.slice(0)
45444
+ }), label);
43997
45445
  } catch (err) {
43998
45446
  endRequest();
43999
45447
  logger.error(`orchestrator: upstream fetch failed for ${ctx.routeKey} → ${model.id}: ${err.message}`);
@@ -44164,6 +45612,10 @@ function splitWireAgent(rawAgent) {
44164
45612
  ...instanceId ? { instanceId } : {}
44165
45613
  };
44166
45614
  }
45615
+ function restPathForWireRequest(pathname, rawAgent) {
45616
+ const prefix = `/wire/${rawAgent}`;
45617
+ return pathname.slice(prefix.length) || "/";
45618
+ }
44167
45619
  async function handleWireRequest(c) {
44168
45620
  const rawAgent = c.req.param("agent");
44169
45621
  if (!rawAgent) return new Response(JSON.stringify({ error: "afw: malformed route" }), {
@@ -44174,8 +45626,7 @@ async function handleWireRequest(c) {
44174
45626
  const req = c.req.raw;
44175
45627
  {
44176
45628
  const reqUrl$1 = new URL(c.req.url);
44177
- const prefix$1 = `/wire/${rawAgent}`;
44178
- const restPath$1 = reqUrl$1.pathname.slice(prefix$1.length) || "/";
45629
+ const restPath$1 = restPathForWireRequest(reqUrl$1.pathname, rawAgent);
44179
45630
  if (req.method === "GET" && isClaudeDesktopModelsRequest(agent, restPath$1)) {
44180
45631
  const body = await synthesizeClaudeDesktopModels();
44181
45632
  return new Response(JSON.stringify(body), {
@@ -44201,8 +45652,7 @@ async function handleWireRequest(c) {
44201
45652
  const routeKey = route.sourceModelId ? `${agent}/${bodyModel}` : `${agent}/*`;
44202
45653
  const policyKey = policyKeyFor(agent, bodyModel, instanceId);
44203
45654
  const reqUrl = new URL(c.req.url);
44204
- const prefix = `/wire/${agent}`;
44205
- const restPath = reqUrl.pathname.slice(prefix.length) || "/";
45655
+ const restPath = restPathForWireRequest(reqUrl.pathname, rawAgent);
44206
45656
  const upstreamBase = route.upstream.replace(/\/$/, "");
44207
45657
  const restWithSlash = restPath.startsWith("/") ? restPath : `/${restPath}`;
44208
45658
  const upstreamUrl = new URL(upstreamBase + restWithSlash + reqUrl.search);
@@ -44631,6 +46081,35 @@ daemonCommand.command("restart").description("Restart the daemon — stop the ru
44631
46081
  }
44632
46082
  });
44633
46083
 
46084
+ //#endregion
46085
+ //#region src/cli/commands/oauth.ts
46086
+ const PROVIDER_KEYS = Object.keys(OAUTH_PROVIDERS);
46087
+ const loginCmd = new Command("login").argument("[provider]", `provider to log in to (${PROVIDER_KEYS.join(", ")})`).description("Log in to a model provider via OAuth and store the token in afw.").action(async (provider) => {
46088
+ try {
46089
+ if (!process$1.stdin.isTTY) {
46090
+ logger.print("oauth login needs an interactive terminal.");
46091
+ process$1.exitCode = 1;
46092
+ return;
46093
+ }
46094
+ const key = provider;
46095
+ if (!key || !PROVIDER_KEYS.includes(key)) {
46096
+ logger.print(`usage: afw oauth login <${PROVIDER_KEYS.join("|")}>`);
46097
+ process$1.exitCode = 1;
46098
+ return;
46099
+ }
46100
+ const def = await oauthLogin(key);
46101
+ if (!def) {
46102
+ process$1.exitCode = 1;
46103
+ return;
46104
+ }
46105
+ logger.print(`\nDone. Register a route to it with \`afw model add\` (pick ${def.label}), or in the dashboard.`);
46106
+ } catch (e) {
46107
+ logger.print(`error: ${e.message}`);
46108
+ process$1.exit(1);
46109
+ }
46110
+ });
46111
+ const oauthCommand = new Command("oauth").description("Manage afw-owned OAuth subscription logins (Anthropic, OpenAI).").addCommand(loginCmd);
46112
+
44634
46113
  //#endregion
44635
46114
  //#region src/cli/commands/tier.ts
44636
46115
  const SINGLE = "A single model";
@@ -44882,7 +46361,7 @@ const onboardCommand = new Command("onboard").description("Set up afw: register
44882
46361
 
44883
46362
  //#endregion
44884
46363
  //#region src/cli/commands/run.ts
44885
- const runCommand = new Command("run").description("Launch one agent instance with its own wire identity — per-instance capture and routing, without touching the agent's shared config.\n Examples:\n afw run --as planner --model claude-opus-4-8 -- claude # keep Opus\n afw run --as worker --model claude-sonnet-4-6 -- claude # downgrade\n afw run --as audit --monitor -- claude # track, never reroute\n afw run --as solo --raw -- claude # bypass afw").argument("<command...>", "the agent command to launch, e.g. `-- claude` (pass it after `--`)").option("--as <label>", "instance label — stable across relaunches; spend accrues to it").option("--model <id>", "route this instance to a single model").option("--monitor", "capture this instance but never reroute (passthrough)").option("--raw", "bypass afw entirely for this instance (no capture, no routing)").option("--agent <id>", "force the agent type instead of inferring it from the command").option("--ephemeral", "remove the instance routing policy when the process exits").action(async (command, opts) => {
46364
+ const runCommand = new Command("run").description("Launch one agent instance with its own wire identity — per-instance capture and routing, without touching the agent's shared config.\n Examples:\n afw run --as planner --model claude-opus-4-8 -- claude # keep Opus\n afw run --as worker --model claude-sonnet-4-6 -- claude # downgrade\n afw run --as audit --monitor -- claude # track, never reroute\n afw run --as solo --raw -- claude # bypass afw").argument("<command...>", "the agent command to launch, e.g. `-- claude` (pass it after `--`)").option("--as <label>", "instance label — stable across relaunches; spend accrues to it").option("--model <id>", "route this instance to a single model").option("--monitor", "capture this instance but never reroute (passthrough)").option("--raw", "bypass afw entirely for this instance (no capture, no routing)").option("--agent <id>", "force the agent type instead of inferring it from the command").option("--ephemeral", "remove the instance routing policy when the process exits").allowUnknownOption().passThroughOptions().action(async (command, opts) => {
44886
46365
  const fail$1 = (m) => {
44887
46366
  logger.print(`error: ${m}`);
44888
46367
  process$1.exit(1);
@@ -44900,7 +46379,7 @@ const runCommand = new Command("run").description("Launch one agent instance wit
44900
46379
  try {
44901
46380
  if (!opts.raw) {
44902
46381
  await ensureDaemonRunning();
44903
- await ensureWireRoute(wiring.agent);
46382
+ await ensureWireRoute(wiring.agent, { modelOverride: opts.model });
44904
46383
  }
44905
46384
  await launchInstance({
44906
46385
  bin,
@@ -45214,6 +46693,7 @@ async function run() {
45214
46693
  program$1.addCommand(runCommand);
45215
46694
  program$1.addCommand(onboardCommand);
45216
46695
  program$1.addCommand(modelCommand);
46696
+ program$1.addCommand(oauthCommand);
45217
46697
  program$1.addCommand(tierCommand);
45218
46698
  program$1.addCommand(keyCommand);
45219
46699
  program$1.addCommand(routeCommand);