@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/README.md +2 -2
- package/dist/{backends-Byh5VYtT.js → backends-BmyOv3bJ.js} +1 -1
- package/dist/bin/afw.js +2303 -823
- package/dist/bin/tools.js +2 -2
- package/dist/{secrets-9JqUBHyw.js → secrets-DJCX60WS.js} +1 -1
- package/dist/{secrets-Bj-gyv53.js → secrets-evRw4cV3.js} +5 -0
- package/package.json +1 -1
- package/ui-dist/assets/index-Cja3pO9A.js +20 -0
- package/ui-dist/index.html +1 -1
- package/ui-dist/assets/index-C9yCeZlD.js +0 -20
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-
|
|
3
|
-
import { activeProviderFor, mutateToolProviders, readToolProviders, searchBaidu, searchBrave, searchDuckDuckGo } from "../backends-
|
|
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 {
|
|
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$
|
|
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:
|
|
3287
|
+
stdio: daemonStdio()
|
|
3267
3288
|
});
|
|
3268
3289
|
child.unref();
|
|
3269
3290
|
for (let i = 0; i < 40; i++) {
|
|
3270
|
-
await sleep$
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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$
|
|
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
|
-
|
|
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$
|
|
4198
|
-
if (!MODEL_APIS$
|
|
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
|
-
|
|
4208
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
4428
|
-
const
|
|
5849
|
+
const key = await promptSecret("API key (leave blank for none): ");
|
|
5850
|
+
const ids = [];
|
|
4429
5851
|
for (;;) {
|
|
4430
|
-
const id = await promptText(
|
|
5852
|
+
const id = await promptText(ids.length === 0 ? "Model id" : "Another model id (blank to finish)");
|
|
4431
5853
|
if (!id) break;
|
|
4432
|
-
|
|
5854
|
+
ids.push(id);
|
|
4433
5855
|
if (!process$1.stdin.isTTY) break;
|
|
4434
5856
|
}
|
|
4435
|
-
if (
|
|
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,
|
|
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")
|
|
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
|
|
4482
|
-
id,
|
|
4483
|
-
providerId,
|
|
4484
|
-
|
|
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} + ${
|
|
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)
|
|
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)
|
|
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$
|
|
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$
|
|
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
|
-
|
|
11764
|
-
|
|
11765
|
-
|
|
11766
|
-
|
|
11767
|
-
|
|
11768
|
-
|
|
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
|
-
|
|
11788
|
-
|
|
11789
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
11817
|
-
rawKey = isObj$
|
|
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$
|
|
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$
|
|
13817
|
+
const exists$2 = await fileExists(filePath);
|
|
12640
13818
|
let originalText = "";
|
|
12641
13819
|
let parsed;
|
|
12642
|
-
if (exists$
|
|
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$
|
|
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:
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
14986
|
-
const server = createServer$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
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
|
|
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
|
|
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 === "
|
|
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
|
-
|
|
23668
|
-
|
|
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
|
|
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) =>
|
|
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
|
|
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)
|
|
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$
|
|
36083
|
-
if (!exists$
|
|
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 === "
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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/
|
|
40994
|
-
|
|
40995
|
-
|
|
40996
|
-
|
|
40997
|
-
|
|
40998
|
-
const
|
|
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
|
-
|
|
41002
|
-
|
|
41003
|
-
|
|
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.
|
|
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)
|
|
41271
|
-
|
|
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
|
|
41279
|
-
*
|
|
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;
|
|
41334
|
-
const CLAUDE_CODE_CLIENT_UA = "claude-cli/2.0.0 (afw;
|
|
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
|
-
|
|
41478
|
-
if (member.api === "
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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,
|
|
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:
|
|
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) =>
|
|
42664
|
-
|
|
42665
|
-
|
|
42666
|
-
|
|
42667
|
-
|
|
42668
|
-
|
|
42669
|
-
|
|
42670
|
-
|
|
42671
|
-
|
|
42672
|
-
|
|
42673
|
-
|
|
42674
|
-
|
|
42675
|
-
|
|
42676
|
-
|
|
42677
|
-
|
|
42678
|
-
|
|
42679
|
-
|
|
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-
|
|
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
|
|
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)}
|
|
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
|
-
|
|
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
|
|
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
|
|
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);
|