super-dsh 0.1.9 → 0.1.10-e
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 +25 -3
- package/agent-agy/cordis.patch.yml +7 -4
- package/agent-agy/dist/agent.d.ts +12 -11
- package/agent-agy/dist/agy-auth-gateway.d.ts +124 -0
- package/agent-agy/dist/agy-auth-gateway.js +233 -0
- package/agent-agy/dist/agy-auth.d.ts +153 -0
- package/agent-agy/dist/agy-auth.js +179 -0
- package/agent-agy/dist/agy-cli-client.d.ts +39 -9
- package/agent-agy/dist/agy-cli-client.js +82 -18
- package/agent-agy/dist/agy-store.d.ts +8 -0
- package/agent-agy/dist/index.d.ts +46 -16
- package/agent-agy/dist/index.js +82 -56
- package/agent-agy/dist/models.d.ts +70 -0
- package/agent-agy/dist/models.js +102 -3
- package/agent-agy/package.json +7 -5
- package/agent-agy-sdk/cordis.patch.yml +58 -0
- package/agent-agy-sdk/dist/adapter.d.ts +15 -0
- package/agent-agy-sdk/dist/adapter.js +42 -0
- package/agent-agy-sdk/dist/agent-preset-agy.d.ts +27 -0
- package/agent-agy-sdk/dist/agent-preset-agy.js +32 -0
- package/agent-agy-sdk/dist/agent.d.ts +138 -0
- package/agent-agy-sdk/dist/agent.js +494 -0
- package/{agent-agy → agent-agy-sdk}/dist/agy-client.d.ts +26 -6
- package/{agent-agy → agent-agy-sdk}/dist/agy-client.js +43 -25
- package/agent-agy-sdk/dist/agy-sessions.d.ts +25 -0
- package/agent-agy-sdk/dist/agy-sessions.js +107 -0
- package/agent-agy-sdk/dist/agy-store.d.ts +39 -0
- package/agent-agy-sdk/dist/agy-store.js +83 -0
- package/agent-agy-sdk/dist/inbox.d.ts +26 -0
- package/agent-agy-sdk/dist/inbox.js +53 -0
- package/agent-agy-sdk/dist/index.d.ts +93 -0
- package/agent-agy-sdk/dist/index.js +476 -0
- package/agent-agy-sdk/dist/models.d.ts +41 -0
- package/agent-agy-sdk/dist/models.js +74 -0
- package/agent-agy-sdk/dist/sdk-venv.d.ts +76 -0
- package/agent-agy-sdk/dist/sdk-venv.js +173 -0
- package/agent-agy-sdk/dist/world-plugin.d.ts +10 -0
- package/agent-agy-sdk/dist/world-plugin.js +82 -0
- package/agent-agy-sdk/package.json +64 -0
- package/agent-claude/dist/adapter.js +106 -31
- package/agent-claude/dist/agent.js +154 -17
- package/agent-claude/dist/claude-client.js +84 -43
- package/agent-claude/dist/claude-launch.js +101 -0
- package/agent-claude/dist/claude-native-config.js +169 -0
- package/agent-claude/dist/claude-provider-gateway.js +340 -0
- package/agent-claude/dist/claude-providers.js +391 -0
- package/agent-claude/dist/claude-route.js +152 -0
- package/agent-claude/dist/index.js +214 -37
- package/agent-claude/dist/models.js +14 -0
- package/agent-claude/dist/sdk-runtime.js +320 -0
- package/agent-claude/package.json +6 -4
- package/agent-codex/README.md +141 -36
- package/agent-codex/cordis.patch.yml +14 -10
- package/agent-codex/dist/adapter.js +64 -28
- package/agent-codex/dist/agent.js +255 -81
- package/agent-codex/dist/app-server-approvals.js +165 -0
- package/agent-codex/dist/app-server-client.js +723 -0
- package/agent-codex/dist/app-server-events.js +432 -0
- package/agent-codex/dist/app-server-protocol.js +95 -0
- package/agent-codex/dist/app-server-wire.js +299 -0
- package/agent-codex/dist/codex-launch.js +78 -0
- package/agent-codex/dist/codex-native-config.js +392 -0
- package/agent-codex/dist/codex-provider-gateway.js +402 -0
- package/agent-codex/dist/codex-providers.js +376 -0
- package/agent-codex/dist/codex-route.js +86 -0
- package/agent-codex/dist/index.js +256 -115
- package/agent-codex/dist/permission.js +75 -1
- package/agent-codex/package.json +1 -2
- package/agent-codex-sdk/README.md +79 -0
- package/agent-codex-sdk/cordis.patch.yml +81 -0
- package/agent-codex-sdk/dist/adapter.js +91 -0
- package/agent-codex-sdk/dist/agent-preset-codex.js +32 -0
- package/agent-codex-sdk/dist/agent.js +1085 -0
- package/agent-codex-sdk/dist/codex-native-config.js +392 -0
- package/agent-codex-sdk/dist/codex-provider-gateway.js +402 -0
- package/agent-codex-sdk/dist/codex-providers.js +376 -0
- package/agent-codex-sdk/dist/codex-route.js +86 -0
- package/{agent-codex/dist/codex-client.js → agent-codex-sdk/dist/codex-sdk-client.js} +253 -32
- package/agent-codex-sdk/dist/codex-sdk-launch.js +92 -0
- package/agent-codex-sdk/dist/codex-store.js +180 -0
- package/agent-codex-sdk/dist/inbox.js +53 -0
- package/agent-codex-sdk/dist/index.js +739 -0
- package/agent-codex-sdk/dist/permission.js +71 -0
- package/agent-codex-sdk/dist/sdk-runtime.js +307 -0
- package/agent-codex-sdk/dist/session-map.js +167 -0
- package/agent-codex-sdk/dist/world-plugin.js +93 -0
- package/agent-codex-sdk/package.json +79 -0
- package/agent-codex-sdk/scripts/fetch-sdk-runtime.mjs +57 -0
- package/agent-omp/README.md +72 -55
- package/agent-omp/dist/agent.js +36 -11
- package/agent-omp/dist/index.js +92 -52
- package/agent-omp/dist/lazy-rpc.js +11 -4
- package/agent-omp/dist/omp-cli.js +114 -97
- package/agent-omp/dist/omp-store.js +12 -4
- package/agent-omp/dist/permission.js +70 -5
- package/agent-omp/dist/rpc-client.js +409 -0
- package/agent-omp/dist/world-plugin.js +36 -3
- package/agent-omp/package.json +2 -6
- package/agent-omp-sdk/README.md +103 -0
- package/agent-omp-sdk/cordis.patch.yml +108 -0
- package/agent-omp-sdk/dist/adapter.d.ts +21 -0
- package/agent-omp-sdk/dist/adapter.js +36 -0
- package/agent-omp-sdk/dist/agent-preset-omp.d.ts +31 -0
- package/agent-omp-sdk/dist/agent-preset-omp.js +32 -0
- package/agent-omp-sdk/dist/agent.d.ts +64 -0
- package/agent-omp-sdk/dist/agent.js +1178 -0
- package/agent-omp-sdk/dist/discovery.d.ts +57 -0
- package/agent-omp-sdk/dist/discovery.js +116 -0
- package/agent-omp-sdk/dist/inbox.d.ts +28 -0
- package/agent-omp-sdk/dist/inbox.js +53 -0
- package/agent-omp-sdk/dist/index.d.ts +60 -0
- package/agent-omp-sdk/dist/index.js +632 -0
- package/agent-omp-sdk/dist/knobs.d.ts +34 -0
- package/agent-omp-sdk/dist/knobs.js +50 -0
- package/agent-omp-sdk/dist/lazy-rpc.d.ts +98 -0
- package/agent-omp-sdk/dist/lazy-rpc.js +178 -0
- package/agent-omp-sdk/dist/mobile/zoom-guard.d.ts +187 -0
- package/agent-omp-sdk/dist/mobile/zoom-guard.js +294 -0
- package/agent-omp-sdk/dist/mobile-boot.d.ts +49 -0
- package/agent-omp-sdk/dist/mobile-boot.js +38 -0
- package/agent-omp-sdk/dist/models.d.ts +39 -0
- package/agent-omp-sdk/dist/models.js +299 -0
- package/agent-omp-sdk/dist/omp-cli.d.ts +67 -0
- package/agent-omp-sdk/dist/omp-cli.js +164 -0
- package/agent-omp-sdk/dist/omp-disk-discovery.d.ts +27 -0
- package/agent-omp-sdk/dist/omp-disk-discovery.js +178 -0
- package/agent-omp-sdk/dist/omp-executable.d.ts +2 -0
- package/agent-omp-sdk/dist/omp-executable.js +30 -0
- package/agent-omp-sdk/dist/omp-store.d.ts +134 -0
- package/agent-omp-sdk/dist/omp-store.js +526 -0
- package/agent-omp-sdk/dist/pairing.d.ts +7 -0
- package/agent-omp-sdk/dist/pairing.js +45 -0
- package/agent-omp-sdk/dist/permission.d.ts +138 -0
- package/agent-omp-sdk/dist/permission.js +161 -0
- package/agent-omp-sdk/dist/protocol.d.ts +189 -0
- package/agent-omp-sdk/dist/rpc-types.d.ts +77 -0
- package/agent-omp-sdk/dist/rpc-types.js +1 -0
- package/agent-omp-sdk/dist/sdk-client.d.ts +160 -0
- package/{agent-omp → agent-omp-sdk}/dist/sdk-client.js +108 -56
- package/agent-omp-sdk/dist/sdk-runtime.d.ts +211 -0
- package/agent-omp-sdk/dist/sdk-runtime.js +418 -0
- package/agent-omp-sdk/dist/sidecar-client.d.ts +87 -0
- package/agent-omp-sdk/dist/sidecar-client.js +197 -0
- package/agent-omp-sdk/dist/store/adapter.d.ts +17 -0
- package/agent-omp-sdk/dist/store/adapter.js +47 -0
- package/agent-omp-sdk/dist/store/db.d.ts +70 -0
- package/agent-omp-sdk/dist/store/db.js +210 -0
- package/agent-omp-sdk/dist/store/index.d.ts +23 -0
- package/agent-omp-sdk/dist/store/index.js +59 -0
- package/agent-omp-sdk/dist/store/reconcile.d.ts +15 -0
- package/agent-omp-sdk/dist/store/reconcile.js +51 -0
- package/agent-omp-sdk/dist/store/schema.d.ts +8 -0
- package/agent-omp-sdk/dist/store/schema.js +40 -0
- package/agent-omp-sdk/dist/transport.d.ts +49 -0
- package/agent-omp-sdk/dist/transport.js +35 -0
- package/agent-omp-sdk/dist/world-home.d.ts +4 -0
- package/agent-omp-sdk/dist/world-home.js +23 -0
- package/agent-omp-sdk/dist/world-plugin.d.ts +10 -0
- package/agent-omp-sdk/dist/world-plugin.js +137 -0
- package/agent-omp-sdk/package.json +87 -0
- package/client/index.js +1204 -93
- package/cordis.patch.yml +64 -0
- package/package.json +17 -8
- package/superd-hub/dist/client/agent-settings.d.ts +210 -0
- package/superd-hub/dist/client/agent-settings.js +300 -0
- package/superd-hub/dist/client/roster.js +6 -0
- package/superd-hub/dist/client/runtime-settings-api.d.ts +76 -0
- package/superd-hub/dist/client/runtime-settings-api.js +137 -0
- package/superd-hub/dist/client/switch.d.ts +165 -0
- package/superd-hub/dist/client/switch.js +295 -0
- package/superd-hub/lib/client/index.js +1204 -93
- package/agent-codex/dist/models.js +0 -152
- package/agent-omp/dist/sidecar-client.js +0 -195
- /package/{agent-agy → agent-agy-sdk}/bridge/agy_bridge.py +0 -0
- /package/{agent-agy → agent-agy-sdk}/bridge/gemini_openai_server.mjs +0 -0
- /package/{agent-codex/dist/codex-events.js → agent-codex-sdk/dist/codex-sdk-events.js} +0 -0
- /package/{agent-omp → agent-omp-sdk}/dist/protocol.js +0 -0
- /package/{agent-omp → agent-omp-sdk}/sidecar/main.ts +0 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-providers.ts — the adapter-owned multi-provider store behind the Hub's
|
|
3
|
+
* Claude settings panel (2026-10-04 multi-provider ruling).
|
|
4
|
+
*
|
|
5
|
+
* Claude Code is a single-provider harness: its own `settings.json` env block
|
|
6
|
+
* names exactly ONE `ANTHROPIC_BASE_URL` with one key, so the LIST of
|
|
7
|
+
* configured providers has nowhere to live in Claude's home, and this module
|
|
8
|
+
* is where it lives instead: `<worldHome>/claude-providers.json` under the
|
|
9
|
+
* world's DSH home (`<dshHome>/agents/claude/`), the location dev-rules §5/§13
|
|
10
|
+
* already assigns to the adapter's own DSH state.
|
|
11
|
+
*
|
|
12
|
+
* This file is the CANONICAL provider list. The native `settings.json` env
|
|
13
|
+
* block is a SOURCE, not a peer: `syncNativeProviders` folds whatever it
|
|
14
|
+
* declares into this list, deduplicated by endpoint URL (the only identity a
|
|
15
|
+
* hand-written settings env reliably carries), and never deletes anything.
|
|
16
|
+
* There is no "selected" provider — which provider a session runs on is DSH's
|
|
17
|
+
* own per-session `model/selection`, and the native home is written by nobody.
|
|
18
|
+
*
|
|
19
|
+
* The file carries API keys, so it is written 0600 with the sibling
|
|
20
|
+
* `.credentials.yaml` posture, and reads are fail-soft: a corrupt or partial
|
|
21
|
+
* store must degrade to "no providers yet", never take the world down. A
|
|
22
|
+
* provider entry that does not survive validation is DROPPED rather than
|
|
23
|
+
* poisoning the whole list.
|
|
24
|
+
*
|
|
25
|
+
* @module agent-claude/claude-providers
|
|
26
|
+
*/
|
|
27
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
28
|
+
import { dirname, join } from "node:path";
|
|
29
|
+
/**
|
|
30
|
+
* A provider id becomes one half of the composite model id and is shown in the
|
|
31
|
+
* panel, so the alphabet is deliberately narrow: lowercase alphanumerics and
|
|
32
|
+
* interior hyphens. (Covers `zai-plan`, `deepseek`, `glm`; rejects spaces,
|
|
33
|
+
* dots, quotes, the composite-key separator `/` and anything else a slug was
|
|
34
|
+
* never meant to carry.)
|
|
35
|
+
*/
|
|
36
|
+
const PROVIDER_ID = /^[a-z0-9][a-z0-9-]*$/;
|
|
37
|
+
/** Longest generated id: the slug part plus its 7-character discriminator. */
|
|
38
|
+
const MAX_ID_LENGTH = 48;
|
|
39
|
+
/** Whether `value` may be used as a provider id. */
|
|
40
|
+
export function isProviderId(value) {
|
|
41
|
+
return typeof value === "string" && PROVIDER_ID.test(value);
|
|
42
|
+
}
|
|
43
|
+
/** Where the store lives: `<worldHome>/claude-providers.json`. */
|
|
44
|
+
export function providerStorePath(worldHome) {
|
|
45
|
+
return join(worldHome, "claude-providers.json");
|
|
46
|
+
}
|
|
47
|
+
/** The empty store — also the fail-soft answer for every unreadable input. */
|
|
48
|
+
function emptyStore() {
|
|
49
|
+
return { providers: [], removed: [] };
|
|
50
|
+
}
|
|
51
|
+
function str(value) {
|
|
52
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
53
|
+
}
|
|
54
|
+
function auth(value) {
|
|
55
|
+
if (value === null || typeof value !== "object")
|
|
56
|
+
return undefined;
|
|
57
|
+
const record = value;
|
|
58
|
+
const kind = record["kind"];
|
|
59
|
+
if (kind !== "authToken" && kind !== "apiKey")
|
|
60
|
+
return undefined;
|
|
61
|
+
// An EMPTY value is a real state, not a malformed entry: it means "this
|
|
62
|
+
// provider is keyed outside Super D", which is how a provider seeded from
|
|
63
|
+
// the user's own settings arrives. See ClaudeProviderAuth.
|
|
64
|
+
const secret = record["value"];
|
|
65
|
+
return typeof secret === "string" ? { kind, value: secret } : undefined;
|
|
66
|
+
}
|
|
67
|
+
function model(value) {
|
|
68
|
+
if (value === null || typeof value !== "object")
|
|
69
|
+
return undefined;
|
|
70
|
+
const record = value;
|
|
71
|
+
const id = str(record["id"]);
|
|
72
|
+
if (id === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
const displayName = str(record["displayName"]);
|
|
75
|
+
const contextWindow = record["contextWindow"];
|
|
76
|
+
return {
|
|
77
|
+
id,
|
|
78
|
+
...(displayName === undefined ? {} : { displayName }),
|
|
79
|
+
...(typeof contextWindow === "number" && Number.isSafeInteger(contextWindow) && contextWindow > 0
|
|
80
|
+
? { contextWindow }
|
|
81
|
+
: {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function aliases(value) {
|
|
85
|
+
if (value === null || typeof value !== "object")
|
|
86
|
+
return undefined;
|
|
87
|
+
const record = value;
|
|
88
|
+
const sonnet = str(record["sonnet"]);
|
|
89
|
+
const opus = str(record["opus"]);
|
|
90
|
+
const haiku = str(record["haiku"]);
|
|
91
|
+
if (sonnet === undefined && opus === undefined && haiku === undefined)
|
|
92
|
+
return undefined;
|
|
93
|
+
return {
|
|
94
|
+
...(sonnet === undefined ? {} : { sonnet }),
|
|
95
|
+
...(opus === undefined ? {} : { opus }),
|
|
96
|
+
...(haiku === undefined ? {} : { haiku }),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Validate one raw entry. Returns `undefined` for anything unusable — an id
|
|
101
|
+
* outside the alphabet, a missing endpoint, an auth shape this build does not
|
|
102
|
+
* understand — so the caller can drop it and keep the rest. An empty `models`
|
|
103
|
+
* list is VALID (see {@link ClaudeProviderEntry.models}).
|
|
104
|
+
*/
|
|
105
|
+
export function providerEntry(value) {
|
|
106
|
+
if (value === null || typeof value !== "object")
|
|
107
|
+
return undefined;
|
|
108
|
+
const record = value;
|
|
109
|
+
const id = record["id"];
|
|
110
|
+
const label = str(record["label"]);
|
|
111
|
+
const baseUrl = str(record["baseUrl"]);
|
|
112
|
+
const credentials = auth(record["auth"]);
|
|
113
|
+
if (!isProviderId(id) || label === undefined || baseUrl === undefined || credentials === undefined) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const rawModels = record["models"];
|
|
117
|
+
const models = (Array.isArray(rawModels) ? rawModels : [])
|
|
118
|
+
.map(model)
|
|
119
|
+
.filter((entry) => entry !== undefined);
|
|
120
|
+
const parsedAliases = aliases(record["aliases"]);
|
|
121
|
+
const origin = record["origin"] === "native" ? "native" : "user";
|
|
122
|
+
return {
|
|
123
|
+
id,
|
|
124
|
+
label,
|
|
125
|
+
baseUrl,
|
|
126
|
+
auth: credentials,
|
|
127
|
+
models,
|
|
128
|
+
...(parsedAliases === undefined ? {} : { aliases: parsedAliases }),
|
|
129
|
+
origin,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Read the store. Fail-soft by construction: a missing file, unparsable JSON,
|
|
134
|
+
* a wrong top-level shape, or individual bad entries all degrade toward an
|
|
135
|
+
* empty (or shorter) list rather than throwing.
|
|
136
|
+
*/
|
|
137
|
+
export function readProviderStore(worldHome) {
|
|
138
|
+
const path = providerStorePath(worldHome);
|
|
139
|
+
if (!existsSync(path))
|
|
140
|
+
return emptyStore();
|
|
141
|
+
let parsed;
|
|
142
|
+
try {
|
|
143
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return emptyStore();
|
|
147
|
+
}
|
|
148
|
+
if (parsed === null || typeof parsed !== "object")
|
|
149
|
+
return emptyStore();
|
|
150
|
+
const record = parsed;
|
|
151
|
+
if (!Array.isArray(record["providers"]))
|
|
152
|
+
return emptyStore();
|
|
153
|
+
const providers = record["providers"]
|
|
154
|
+
.map(providerEntry)
|
|
155
|
+
.filter((entry) => entry !== undefined);
|
|
156
|
+
const rawRemoved = record["removed"];
|
|
157
|
+
const removed = (Array.isArray(rawRemoved) ? rawRemoved : [])
|
|
158
|
+
.filter((value) => typeof value === "string" && value.trim() !== "");
|
|
159
|
+
return { providers, removed };
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Persist the store atomically (tmp + rename) and 0600 — the file holds API
|
|
163
|
+
* keys in the clear, exactly like the sibling `.credentials.yaml`, so it must
|
|
164
|
+
* never be group- or world-readable even for the instant before a rewrite.
|
|
165
|
+
*/
|
|
166
|
+
export function writeProviderStore(worldHome, store) {
|
|
167
|
+
const path = providerStorePath(worldHome);
|
|
168
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
169
|
+
const temp = `${path}.tmp-${process.pid}`;
|
|
170
|
+
writeFileSync(temp, `${JSON.stringify(store, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
171
|
+
renameSync(temp, path);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Insert or replace one provider by id, preserving list order.
|
|
175
|
+
*
|
|
176
|
+
* An explicit add also clears the endpoint's removal tombstone: re-adding a
|
|
177
|
+
* provider is the user overruling their own earlier removal, and leaving the
|
|
178
|
+
* tombstone would make the next sync skip it again.
|
|
179
|
+
*/
|
|
180
|
+
export function upsertProvider(store, entry) {
|
|
181
|
+
const at = store.providers.findIndex((existing) => existing.id === entry.id);
|
|
182
|
+
const providers = at === -1
|
|
183
|
+
? [...store.providers, entry]
|
|
184
|
+
: store.providers.map((existing, index) => (index === at ? entry : existing));
|
|
185
|
+
const key = endpointKey(entry.baseUrl);
|
|
186
|
+
// The endpoint's tombstone is FILTERED OUT, not carried forward: leaving it
|
|
187
|
+
// would make the next sync skip the endpoint the user just re-added.
|
|
188
|
+
const removed = store.removed.filter((candidate) => candidate !== key);
|
|
189
|
+
return { providers, removed };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Drop one provider, and TOMBSTONE its endpoint so the reconcile cannot bring
|
|
193
|
+
* it back: the user's own `settings.json` still declares it, and a list editor
|
|
194
|
+
* whose delete button undoes itself every ten minutes is worse than no button.
|
|
195
|
+
*
|
|
196
|
+
* Removing from this list is still not "unconfiguring Claude Code": nothing
|
|
197
|
+
* native is touched, and the user's own CLI keeps running whatever their file
|
|
198
|
+
* says.
|
|
199
|
+
*/
|
|
200
|
+
export function removeProvider(store, id) {
|
|
201
|
+
const entry = store.providers.find((candidate) => candidate.id === id);
|
|
202
|
+
const providers = store.providers.filter((candidate) => candidate.id !== id);
|
|
203
|
+
if (entry === undefined)
|
|
204
|
+
return { providers, removed: store.removed };
|
|
205
|
+
const key = endpointKey(entry.baseUrl);
|
|
206
|
+
const removed = store.removed.includes(key) ? store.removed : [...store.removed, key];
|
|
207
|
+
return { providers, removed };
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The comparison key for "the same endpoint". A settings env block has no
|
|
211
|
+
* identity beyond its `ANTHROPIC_BASE_URL`, and a hand-written one carries no
|
|
212
|
+
* other identity worth trusting — so the endpoint IS the dedupe key. Case, a
|
|
213
|
+
* trailing slash, and the default port are noise.
|
|
214
|
+
*
|
|
215
|
+
* @param baseUrl - a provider's `ANTHROPIC_BASE_URL` (any shape; unparsable input is compared verbatim).
|
|
216
|
+
* @returns a stable key.
|
|
217
|
+
*/
|
|
218
|
+
export function endpointKey(baseUrl) {
|
|
219
|
+
const trimmed = baseUrl.trim();
|
|
220
|
+
let parsed;
|
|
221
|
+
try {
|
|
222
|
+
parsed = new URL(trimmed);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return trimmed.toLowerCase();
|
|
226
|
+
}
|
|
227
|
+
const path = parsed.pathname.replace(/\/+$/, "");
|
|
228
|
+
const port = parsed.port === "" || (parsed.protocol === "https:" && parsed.port === "443")
|
|
229
|
+
|| (parsed.protocol === "http:" && parsed.port === "80")
|
|
230
|
+
? ""
|
|
231
|
+
: `:${parsed.port}`;
|
|
232
|
+
return `${parsed.protocol}//${parsed.hostname.toLowerCase()}${port}${path}`;
|
|
233
|
+
}
|
|
234
|
+
/** Reduce arbitrary text to the provider-id alphabet, collapsing runs of junk. */
|
|
235
|
+
function slugify(value) {
|
|
236
|
+
const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
237
|
+
return slug === "" ? "provider" : slug.slice(0, MAX_ID_LENGTH - 8);
|
|
238
|
+
}
|
|
239
|
+
/** A short, stable discriminator so two endpoints sharing a host stay distinct. */
|
|
240
|
+
function discriminator(value) {
|
|
241
|
+
let hash = 0x811c9dc5;
|
|
242
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
243
|
+
hash ^= value.charCodeAt(index);
|
|
244
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
245
|
+
}
|
|
246
|
+
return hash.toString(36).padStart(6, "0").slice(-6);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The provider id for an endpoint. A settings env block carries no provider
|
|
250
|
+
* name at all, so this is the path every folded provider takes, and the one a
|
|
251
|
+
* UI-added provider reuses.
|
|
252
|
+
*
|
|
253
|
+
* @param baseUrl - the provider's endpoint.
|
|
254
|
+
* @returns a slug that satisfies {@link isProviderId}.
|
|
255
|
+
*/
|
|
256
|
+
export function providerIdFromEndpoint(baseUrl) {
|
|
257
|
+
const key = endpointKey(baseUrl);
|
|
258
|
+
let host = "provider";
|
|
259
|
+
let path = "";
|
|
260
|
+
try {
|
|
261
|
+
const parsed = new URL(key);
|
|
262
|
+
host = parsed.hostname;
|
|
263
|
+
path = parsed.pathname;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// keep the defaults
|
|
267
|
+
}
|
|
268
|
+
return `${slugify(`${host}${path}`)}-${discriminator(key)}`;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Make `wanted` usable and unique within `taken`.
|
|
272
|
+
*
|
|
273
|
+
* @param wanted - the preferred id (usually a generated slug).
|
|
274
|
+
* @param taken - ids already claimed by other entries.
|
|
275
|
+
* @returns a valid id not present in `taken`.
|
|
276
|
+
*/
|
|
277
|
+
export function uniqueProviderId(wanted, taken) {
|
|
278
|
+
const used = new Set(taken);
|
|
279
|
+
const base = isProviderId(wanted) ? wanted : slugify(wanted);
|
|
280
|
+
if (!used.has(base))
|
|
281
|
+
return base;
|
|
282
|
+
const salted = `${base.slice(0, MAX_ID_LENGTH - 7)}-${discriminator(wanted)}`;
|
|
283
|
+
if (!used.has(salted))
|
|
284
|
+
return salted;
|
|
285
|
+
for (let index = 2;; index += 1) {
|
|
286
|
+
const candidate = `${base.slice(0, MAX_ID_LENGTH - 4)}-${index}`;
|
|
287
|
+
if (!used.has(candidate))
|
|
288
|
+
return candidate;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/** Append the models of `incoming` that `existing` does not already carry. */
|
|
292
|
+
function unionModels(existing, incoming) {
|
|
293
|
+
const known = new Set(existing.map((entry) => entry.id));
|
|
294
|
+
const additions = [];
|
|
295
|
+
for (const candidate of incoming) {
|
|
296
|
+
// Same validation a store read applies: junk in an import is skipped, not
|
|
297
|
+
// propagated — and a duplicate id (even twice within one import) is noise.
|
|
298
|
+
const parsed = model(candidate);
|
|
299
|
+
if (parsed === undefined || known.has(parsed.id))
|
|
300
|
+
continue;
|
|
301
|
+
known.add(parsed.id);
|
|
302
|
+
additions.push(parsed);
|
|
303
|
+
}
|
|
304
|
+
return additions.length === 0 ? existing : [...existing, ...additions];
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Structural auth equality.
|
|
308
|
+
*
|
|
309
|
+
* Reference equality is wrong here: a native sync re-parses the settings on
|
|
310
|
+
* every tick, so the incoming auth object is a FRESH one each time and `!==`
|
|
311
|
+
* would report a change forever. That is not a cosmetic difference — it makes
|
|
312
|
+
* the "only write when something moved" contract fail, so every scheduled
|
|
313
|
+
* reconcile would rewrite a 0600 secrets file.
|
|
314
|
+
*/
|
|
315
|
+
function sameAuth(left, right) {
|
|
316
|
+
return left.kind === right.kind && left.value === right.value;
|
|
317
|
+
}
|
|
318
|
+
/** Structural alias equality — same reasoning as {@link sameAuth}. */
|
|
319
|
+
function sameAliases(left, right) {
|
|
320
|
+
if (left === undefined || right === undefined)
|
|
321
|
+
return left === right;
|
|
322
|
+
return left.sonnet === right.sonnet && left.opus === right.opus && left.haiku === right.haiku;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Fold the providers a native settings env declares into the canonical store.
|
|
326
|
+
*
|
|
327
|
+
* Identity is the endpoint (see {@link endpointKey}); an import whose endpoint
|
|
328
|
+
* already matches an entry refreshes that entry's endpoint, model list, aliases
|
|
329
|
+
* and — only when the entry holds no secret of its own — its auth shape.
|
|
330
|
+
* Anything unmatched is appended, labelled with its endpoint URL, because that
|
|
331
|
+
* is the only name a settings env block gives it. Nothing is ever deleted
|
|
332
|
+
* here: a provider dropped from the settings may still be one the user uses
|
|
333
|
+
* through Super D. The one exception runs the other way — an endpoint the user
|
|
334
|
+
* REMOVED in the panel is skipped, so their removal is not undone by the next
|
|
335
|
+
* reconcile.
|
|
336
|
+
*
|
|
337
|
+
* @param store - the canonical list.
|
|
338
|
+
* @param imported - providers parsed from the native settings env, in file order.
|
|
339
|
+
* @returns the new store plus which ids were added and which were refreshed.
|
|
340
|
+
*/
|
|
341
|
+
export function syncNativeProviders(store, imported) {
|
|
342
|
+
const providers = [...store.providers];
|
|
343
|
+
const added = [];
|
|
344
|
+
const updated = [];
|
|
345
|
+
for (const incoming of imported) {
|
|
346
|
+
const baseUrl = incoming.baseUrl.trim();
|
|
347
|
+
if (baseUrl === "")
|
|
348
|
+
continue;
|
|
349
|
+
const key = endpointKey(baseUrl);
|
|
350
|
+
if (store.removed.includes(key))
|
|
351
|
+
continue;
|
|
352
|
+
const at = providers.findIndex((entry) => endpointKey(entry.baseUrl) === key);
|
|
353
|
+
if (at === -1) {
|
|
354
|
+
const id = uniqueProviderId(providerIdFromEndpoint(baseUrl), providers.map((entry) => entry.id));
|
|
355
|
+
providers.push({
|
|
356
|
+
id,
|
|
357
|
+
label: baseUrl,
|
|
358
|
+
baseUrl,
|
|
359
|
+
auth: incoming.auth,
|
|
360
|
+
models: unionModels([], incoming.models ?? []),
|
|
361
|
+
...(incoming.aliases === undefined ? {} : { aliases: incoming.aliases }),
|
|
362
|
+
origin: "native",
|
|
363
|
+
});
|
|
364
|
+
added.push(id);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const entry = providers[at];
|
|
368
|
+
const models = unionModels(entry.models, incoming.models ?? []);
|
|
369
|
+
// The native env block is the user's own statement about how Claude Code
|
|
370
|
+
// should authenticate, but a secret Super D already holds must not be
|
|
371
|
+
// dropped in favour of it — an EMPTY value is the only shape that defers
|
|
372
|
+
// ("Super D holds no copy"), so only that one follows the native file.
|
|
373
|
+
const adoptAuth = entry.auth.value === "";
|
|
374
|
+
const next = {
|
|
375
|
+
...entry,
|
|
376
|
+
baseUrl,
|
|
377
|
+
...(adoptAuth ? { auth: incoming.auth } : {}),
|
|
378
|
+
models,
|
|
379
|
+
...(incoming.aliases === undefined ? {} : { aliases: incoming.aliases }),
|
|
380
|
+
};
|
|
381
|
+
const changed = next.baseUrl !== entry.baseUrl
|
|
382
|
+
|| next.models !== entry.models
|
|
383
|
+
|| !sameAuth(next.auth, entry.auth)
|
|
384
|
+
|| !sameAliases(next.aliases, entry.aliases);
|
|
385
|
+
if (changed) {
|
|
386
|
+
providers[at] = next;
|
|
387
|
+
updated.push(entry.id);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return { store: { providers, removed: store.removed }, added, updated };
|
|
391
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-route.ts — which provider/model a Claude turn must run
|
|
3
|
+
* (2026-10-04 multi-provider rework).
|
|
4
|
+
*
|
|
5
|
+
* This is the whole of Super D's model-selection policy, and the point is that
|
|
6
|
+
* there is almost none: the precedence below is the harness's own
|
|
7
|
+
* (`sessionController.selectionFor(agent).current`), so a session's route is
|
|
8
|
+
* decided by DSH exactly as it would be for a native agent, and Super D only
|
|
9
|
+
* has to read it.
|
|
10
|
+
*
|
|
11
|
+
* pending the durable `modelSelection` projection's `pending` — a switch
|
|
12
|
+
* made through `session/selectModel` that no `request/header` has
|
|
13
|
+
* retired yet. This is what makes a mid-turn switch land on the NEXT
|
|
14
|
+
* turn, for free: the harness records the intent and the next
|
|
15
|
+
* request consumes it.
|
|
16
|
+
* logged the last `request/header` snapshot — what the session ACTUALLY ran
|
|
17
|
+
* last. It is also what lets a resumed session name the provider
|
|
18
|
+
* that produced its transcript.
|
|
19
|
+
* preferred `ctx.agentDefaultModel` — the deployment default for a session
|
|
20
|
+
* that has never run.
|
|
21
|
+
* fallback the agent's own reconciled options — the pair the factory built
|
|
22
|
+
* from the provider store and the user's native `settings.json`.
|
|
23
|
+
*
|
|
24
|
+
* The Dash model selector, though, speaks one id per model, and with more than
|
|
25
|
+
* one provider a bare CLI alias (`glm-5.2`) is ambiguous. The catalog therefore
|
|
26
|
+
* mints every entry's id as `provider/model` (`compositeModelId`), and a
|
|
27
|
+
* selection's id splits back into its halves here (`parseCompositeModelId`) —
|
|
28
|
+
* on the FIRST slash, because a provider id is slug-alphabet and never carries
|
|
29
|
+
* one while a model id may.
|
|
30
|
+
*
|
|
31
|
+
* One shape predates the per-provider routes and is still in the wild: sessions
|
|
32
|
+
* created before the rework pinned the hardcoded `claude` route with the
|
|
33
|
+
* COMPOSITE id as their model half (`{provider: "claude", model: "glm/glm-5.3"}`).
|
|
34
|
+
* Read raw it names no store entry, so the session would silently relaunch on
|
|
35
|
+
* the native endpoint; {@link routeOf} revives it by splitting that one legacy
|
|
36
|
+
* shape, and only that one — a new-shape selection's raw model id may carry a
|
|
37
|
+
* slash of its own (`zai-plan` + `anthropic/claude-sonnet-4`).
|
|
38
|
+
*
|
|
39
|
+
* Kept out of `agent.ts` so the precedence can be pinned without standing up
|
|
40
|
+
* a Session, a Context and a client: it is the one piece of this feature whose
|
|
41
|
+
* mistakes are silent.
|
|
42
|
+
*
|
|
43
|
+
* @module agent-claude/claude-route
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* The route id every selection was pinned under BEFORE the per-provider-route
|
|
47
|
+
* rework. A local literal on purpose: importing `CLAUDE_PROVIDER_ID` from
|
|
48
|
+
* `adapter.ts` would close a module cycle (`adapter → models → claude-route`).
|
|
49
|
+
*/
|
|
50
|
+
const LEGACY_ROUTE_ID = "claude";
|
|
51
|
+
/**
|
|
52
|
+
* Read a route out of one candidate value.
|
|
53
|
+
*
|
|
54
|
+
* Every candidate arrives as untyped projection/config data, so this validates
|
|
55
|
+
* structurally and answers `undefined` for anything incomplete. A half-filled
|
|
56
|
+
* route must never win the precedence: falling through to a complete one is
|
|
57
|
+
* always better than launching a child with an empty model.
|
|
58
|
+
*
|
|
59
|
+
* @param value - a candidate route (a `ModelSelection`-shaped object, or junk).
|
|
60
|
+
* @returns the route, or `undefined` when it does not name both halves.
|
|
61
|
+
*/
|
|
62
|
+
export function routeOf(value) {
|
|
63
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
64
|
+
return undefined;
|
|
65
|
+
const record = value;
|
|
66
|
+
const provider = record["provider"];
|
|
67
|
+
const model = record["model"];
|
|
68
|
+
if (typeof provider !== "string" || provider === "" || typeof model !== "string" || model === "")
|
|
69
|
+
return undefined;
|
|
70
|
+
// Revive a selection from the pre-per-provider-route architecture: the route
|
|
71
|
+
// half was hardcoded `claude` and the model half was the COMPOSITE id, so a
|
|
72
|
+
// resumed session hands back `{provider: "claude", model: "glm/glm-5.3"}`.
|
|
73
|
+
// Read raw that pair misses the provider store, and the agent falls back to
|
|
74
|
+
// NATIVE_LAUNCH — the session quietly resumes on the native endpoint instead
|
|
75
|
+
// of its pinned provider until the user re-picks. Keyed on the provider half
|
|
76
|
+
// so a new-shape selection passes through untouched, even when its raw model
|
|
77
|
+
// id carries a slash of its own.
|
|
78
|
+
if (provider === LEGACY_ROUTE_ID)
|
|
79
|
+
return parseCompositeModelId(model) ?? { provider, model };
|
|
80
|
+
return { provider, model };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve the route for the next request by the harness's own precedence.
|
|
84
|
+
*
|
|
85
|
+
* @param candidates - the candidates in descending priority: `pending`,
|
|
86
|
+
* `logged`, `preferred`, `fallback`. `preferred` is the harness default
|
|
87
|
+
* (`agentDefaultModel`); `fallback` is the factory's own options.
|
|
88
|
+
* @returns the first complete route, or `undefined` when none is usable.
|
|
89
|
+
*/
|
|
90
|
+
export function resolveRoute(candidates) {
|
|
91
|
+
return routeOf(candidates.pending)
|
|
92
|
+
?? routeOf(candidates.logged)
|
|
93
|
+
?? routeOf(candidates.preferred)
|
|
94
|
+
?? routeOf(candidates.fallback);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Mint the Dash-facing model id for one provider's model: `${provider}/${model}`.
|
|
98
|
+
*
|
|
99
|
+
* The catalog publishes this composite as the entry id because the selector
|
|
100
|
+
* picks by a single id and a bare CLI alias is ambiguous across providers. The
|
|
101
|
+
* provider half is a store id (slug alphabet — `isProviderId` in
|
|
102
|
+
* claude-providers.ts), so it can never itself contain the separator.
|
|
103
|
+
*
|
|
104
|
+
* @param provider - the store provider id.
|
|
105
|
+
* @param model - the real model id the provider serves.
|
|
106
|
+
*/
|
|
107
|
+
export function compositeModelId(provider, model) {
|
|
108
|
+
return `${provider}/${model}`;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Split a composite model id back into its halves, on the FIRST `/` only.
|
|
112
|
+
*
|
|
113
|
+
* The split is anchored at the first slash because the MODEL half may carry
|
|
114
|
+
* slashes of its own while the provider half never does; any later split point
|
|
115
|
+
* would shred the model half and would not round-trip through
|
|
116
|
+
* {@link compositeModelId}.
|
|
117
|
+
*
|
|
118
|
+
* @param id - a composite model id (`provider/model`), e.g. a selection the
|
|
119
|
+
* catalog minted.
|
|
120
|
+
* @returns the halves, or `undefined` when there is no `/` at all or either
|
|
121
|
+
* half is empty — the same completeness rule `routeOf` enforces.
|
|
122
|
+
*/
|
|
123
|
+
export function parseCompositeModelId(id) {
|
|
124
|
+
const slash = id.indexOf("/");
|
|
125
|
+
if (slash === -1)
|
|
126
|
+
return undefined;
|
|
127
|
+
const provider = id.slice(0, slash);
|
|
128
|
+
const model = id.slice(slash + 1);
|
|
129
|
+
if (provider === "" || model === "")
|
|
130
|
+
return undefined;
|
|
131
|
+
return { provider, model };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The route a brand-new session's log is SEEDED with.
|
|
135
|
+
*
|
|
136
|
+
* The single argument is deliberate. The factory's reconciled options are the
|
|
137
|
+
* only acceptable source, because they are already ONE pair: a first attempt
|
|
138
|
+
* at this (the codex line) built the seed from the native provider id and the
|
|
139
|
+
* deployment default model independently, which produced
|
|
140
|
+
* `zhipu-cn/deepseek-flash` — a route no provider serves, and which had to be
|
|
141
|
+
* caught by reading a live session's projection rather than by any unit test.
|
|
142
|
+
* Taking one object makes that mistake unrepresentable.
|
|
143
|
+
*
|
|
144
|
+
* @param options - the factory's reconciled agent options.
|
|
145
|
+
* @returns the seed. The declared input already names both halves, so the
|
|
146
|
+
* `routeOf` screen inside always answers a route for it; a pair that somehow
|
|
147
|
+
* arrives half-empty surfaces as `undefined` to the caller's own guard rather
|
|
148
|
+
* than as a broken seed.
|
|
149
|
+
*/
|
|
150
|
+
export function routeSeed(options) {
|
|
151
|
+
return routeOf(options);
|
|
152
|
+
}
|