github-router 0.3.282 → 0.3.285
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{attribution-settings-srL3gBR9.js → attribution-settings-B8M2fvhz.js} +20 -8
- package/dist/{attribution-settings-srL3gBR9.js.map → attribution-settings-B8M2fvhz.js.map} +1 -1
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/{claude-DJQjA5oQ.js → claude-e-JIQGhR.js} +12 -12
- package/dist/claude-e-JIQGhR.js.map +1 -0
- package/dist/{codex-BAXemWpJ.js → codex-DSnVZ9oK.js} +4 -4
- package/dist/{codex-BAXemWpJ.js.map → codex-DSnVZ9oK.js.map} +1 -1
- package/dist/engine-DMveEa9x.js +2 -0
- package/dist/{gate-discovery-BwtiYKvW.js → gate-discovery-BCFwLm0q.js} +2 -2
- package/dist/{gate-discovery-BwtiYKvW.js.map → gate-discovery-BCFwLm0q.js.map} +1 -1
- package/dist/{internal-first-mate-guard-CQdXWjNz.js → internal-first-mate-guard-B7p4NttK.js} +1 -1
- package/dist/{internal-first-mate-guard-DMgHg2s6.js → internal-first-mate-guard-DOgFVki5.js} +2 -2
- package/dist/{internal-first-mate-guard-DMgHg2s6.js.map → internal-first-mate-guard-DOgFVki5.js.map} +1 -1
- package/dist/{internal-stop-hook-514RJeHO.js → internal-stop-hook-Cf-7w3RH.js} +2 -2
- package/dist/{internal-stop-hook-514RJeHO.js.map → internal-stop-hook-Cf-7w3RH.js.map} +1 -1
- package/dist/{internal-worker-guard-VucpClSi.js → internal-worker-guard-CKgYFiFO.js} +2 -2
- package/dist/{internal-worker-guard-VucpClSi.js.map → internal-worker-guard-CKgYFiFO.js.map} +1 -1
- package/dist/main.js +7 -7
- package/dist/{peer-mcp-personas-l4P4s1fK.js → peer-mcp-personas-V6stFvpq.js} +689 -113
- package/dist/peer-mcp-personas-V6stFvpq.js.map +1 -0
- package/dist/{provision-DlT34zcf.js → provision-BpL6gZIt.js} +2 -2
- package/dist/{provision-DlT34zcf.js.map → provision-BpL6gZIt.js.map} +1 -1
- package/dist/{serve-orGUartp.js → serve-CXUf7RtJ.js} +11 -11
- package/dist/{serve-orGUartp.js.map → serve-CXUf7RtJ.js.map} +1 -1
- package/dist/{server-setup-BRBe8gW4.js → server-setup-Bppjt9xO.js} +21 -203
- package/dist/server-setup-Bppjt9xO.js.map +1 -0
- package/dist/{start-sutbbkwc.js → start-C1-jrHfU.js} +3 -3
- package/dist/{start-sutbbkwc.js.map → start-C1-jrHfU.js.map} +1 -1
- package/dist/token-CnlB0884.js.map +1 -1
- package/dist/{worker-dispatch-4O3IWtP0.js → worker-dispatch-zW8Zi69V.js} +8 -4
- package/dist/{worker-dispatch-4O3IWtP0.js.map → worker-dispatch-zW8Zi69V.js.map} +1 -1
- package/package.json +1 -1
- package/dist/claude-DJQjA5oQ.js.map +0 -1
- package/dist/engine-BvLgh1QI.js +0 -2
- package/dist/peer-mcp-personas-l4P4s1fK.js.map +0 -1
- package/dist/server-setup-BRBe8gW4.js.map +0 -1
|
@@ -163,6 +163,221 @@ function withOneMSuffix(id) {
|
|
|
163
163
|
if (oneMContextDisabled()) return id;
|
|
164
164
|
return catalogAdvertises1M(id) ? `${id}[1m]` : id;
|
|
165
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Decorate a slug the USER named — a `-m` argument or a launcher default — with
|
|
168
|
+
* `[1m]` iff the model it actually RESOLVES to serves >=1M context.
|
|
169
|
+
*
|
|
170
|
+
* The difference from `withOneMSuffix` is the resolution step, and it exists
|
|
171
|
+
* because the two functions are handed different kinds of string. Every
|
|
172
|
+
* `withOneMSuffix` caller already holds a concrete catalog id from a catalog
|
|
173
|
+
* walk, so an exact-id match is both sufficient and the safer rule: inferring a
|
|
174
|
+
* match there could attach `[1m]` to a sibling that does not serve 1M. A lead
|
|
175
|
+
* slug is the opposite case — it is whatever the user typed, or an
|
|
176
|
+
* Anthropic-published dashed slug like `claude-opus-4-8` that the catalog
|
|
177
|
+
* carries in dotted form. Exact-id matching answers "no 1M" for those purely
|
|
178
|
+
* because it never found the entry, which is the silent under-accounting this
|
|
179
|
+
* function exists to stop.
|
|
180
|
+
*
|
|
181
|
+
* Resolving first also picks up the `-1m` SIBLING shape for free:
|
|
182
|
+
* `resolveModel`'s opus family preference maps `claude-opus-4-7` onto
|
|
183
|
+
* `claude-opus-4.7-1m-internal` when that is what the tier carries, and the
|
|
184
|
+
* sibling's own advertised window then answers the question. That is the same
|
|
185
|
+
* dual-signal conclusion `pickClaudeDefault` reaches for the family shorthand,
|
|
186
|
+
* so the two paths cannot disagree about a family both can be asked about.
|
|
187
|
+
*
|
|
188
|
+
* Idempotent: a slug that already carries the bracket is returned unchanged, so
|
|
189
|
+
* a user who pins `-m claude-opus-5[1m]` by hand does not get `[1m][1m]`. That
|
|
190
|
+
* early return deliberately does NOT re-validate the pin against the catalog.
|
|
191
|
+
* `-m claude-haiku-4-5[1m]` therefore survives even though Haiku 4.5 is a 200K
|
|
192
|
+
* model — the same as before this function existed, and `resolveModel` already
|
|
193
|
+
* warns loudly about exactly that case. Stripping a bracket the user typed
|
|
194
|
+
* would be the surprising behaviour, and it would be the only place in the
|
|
195
|
+
* launcher that overrides an explicit `-m`.
|
|
196
|
+
*
|
|
197
|
+
* A repeat can still arrive from the CLIENT side rather than from here: the
|
|
198
|
+
* `/model` picker rows are seeded already decorated, and Claude Code's alias
|
|
199
|
+
* path appends its own bracket (`getDefaultSonnetModel() + '[1m]'`), so
|
|
200
|
+
* selecting `sonnet[1m]` puts `claude-sonnet-5[1m][1m]` on the wire. That
|
|
201
|
+
* resolves to the same bare id — `resolveModel`'s strip recurses — and Claude
|
|
202
|
+
* Code's own detector is unanchored, so local accounting is right too. Pinned
|
|
203
|
+
* by a regression test in `tests/lib-utils.test.ts`.
|
|
204
|
+
*
|
|
205
|
+
* Degrades the same safe direction as everything else here. An unpopulated
|
|
206
|
+
* catalog makes `resolveModel` a pass-through and `catalogAdvertises1M` false,
|
|
207
|
+
* so the slug stays bare and Claude Code accounts at its conservative 200K
|
|
208
|
+
* default — under-accounting, never overflow.
|
|
209
|
+
*/
|
|
210
|
+
function withOneMSuffixForLead(slug) {
|
|
211
|
+
if (oneMContextDisabled()) return slug;
|
|
212
|
+
if (/\[1m\]$/i.test(slug)) return slug;
|
|
213
|
+
return catalogAdvertises1M(resolveModel(slug)) ? `${slug}[1m]` : slug;
|
|
214
|
+
}
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/services/copilot/endpoint.ts
|
|
217
|
+
/**
|
|
218
|
+
* Catalog spellings that mean each of our two clients. Copilot is not
|
|
219
|
+
* self-consistent about the `/v1` prefix — the live catalog advertises
|
|
220
|
+
* `/v1/messages` prefixed but `/chat/completions` bare, and this repo's own
|
|
221
|
+
* fixtures carry both forms — so an exact-match on the bare spelling alone
|
|
222
|
+
* silently misses a real shape. `src/lib/model-validation.ts` already
|
|
223
|
+
* normalizes the same way (`ENDPOINT_ALIASES`); this keeps the two agreeing.
|
|
224
|
+
*
|
|
225
|
+
* Matching is EXACT against this set, never a suffix/`includes` test: a
|
|
226
|
+
* `ws:/responses` (websocket transport) entry is NOT the `/responses` HTTP
|
|
227
|
+
* client and must keep resolving to "serves neither".
|
|
228
|
+
*/
|
|
229
|
+
const CHAT_ENDPOINTS = /* @__PURE__ */ new Set(["/chat/completions", "/v1/chat/completions"]);
|
|
230
|
+
const RESPONSES_ENDPOINTS = /* @__PURE__ */ new Set(["/responses", "/v1/responses"]);
|
|
231
|
+
/**
|
|
232
|
+
* Decide which endpoint to call for a model from its catalog
|
|
233
|
+
* `supported_endpoints`. Prefers `/chat/completions` when available (the
|
|
234
|
+
* simpler, more widely-supported shape) and falls back to `/responses` for
|
|
235
|
+
* models that ONLY serve the Responses API — the gpt-5.x family except
|
|
236
|
+
* `gpt-5-mini` / `gpt-5.4` (e.g. `gpt-5.4-mini`, `gpt-5.5`, the
|
|
237
|
+
* `*-codex` models). Returns undefined when the model serves neither, so a
|
|
238
|
+
* caller can skip it rather than 400 on `unsupported_api_for_model`.
|
|
239
|
+
*
|
|
240
|
+
* A model that OMITS `supported_endpoints` is treated as chat-eligible: the
|
|
241
|
+
* catalog historically omits the field for chat-default models, and
|
|
242
|
+
* excluding those would be a worse regression than the gap this guards.
|
|
243
|
+
*/
|
|
244
|
+
function pickEndpoint(model) {
|
|
245
|
+
const eps = model.supported_endpoints;
|
|
246
|
+
if (!eps || eps.length === 0) return "chat";
|
|
247
|
+
if (eps.some((e) => CHAT_ENDPOINTS.has(e))) return "chat";
|
|
248
|
+
if (eps.some((e) => RESPONSES_ENDPOINTS.has(e))) return "responses";
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* `pickEndpoint` by model id against the live catalog, WITHOUT collapsing
|
|
252
|
+
* "absent from the catalog" into "serves neither of our endpoints".
|
|
253
|
+
*
|
|
254
|
+
* This function deliberately has no default. The predecessor
|
|
255
|
+
* (`endpointForModelId`) returned `pickEndpoint(found) ?? "chat"`, which
|
|
256
|
+
* coerced both cases to "chat" — defensible for an unknown id, silently wrong
|
|
257
|
+
* for a catalog model serving only, say, `/v1/messages`: the caller would drive
|
|
258
|
+
* it through the chat client and get an opaque upstream 400 with no local
|
|
259
|
+
* signal about the real cause. `src/lib/browser-mcp/compressor.ts` already
|
|
260
|
+
* treats that case correctly (`if (!endpoint) continue`); this makes the same
|
|
261
|
+
* distinction available to callers that resolve by id.
|
|
262
|
+
*
|
|
263
|
+
* Callers that legitimately want the chat default for an unknown id can still
|
|
264
|
+
* have it — they just have to write it, per case, on purpose.
|
|
265
|
+
*/
|
|
266
|
+
function resolveEndpointForModelId(id) {
|
|
267
|
+
const found = state.models?.data?.find((m) => m.id === id);
|
|
268
|
+
if (!found) return { kind: "unknown-model" };
|
|
269
|
+
const endpoint = pickEndpoint(found);
|
|
270
|
+
if (endpoint) return {
|
|
271
|
+
kind: "endpoint",
|
|
272
|
+
endpoint
|
|
273
|
+
};
|
|
274
|
+
return {
|
|
275
|
+
kind: "unreachable",
|
|
276
|
+
endpoints: found.supported_endpoints ?? []
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
280
|
+
//#region src/lib/anthropic-translate/classifier.ts
|
|
281
|
+
/**
|
|
282
|
+
* Routing classifier for `POST /v1/messages`.
|
|
283
|
+
*
|
|
284
|
+
* Claude Code speaks the Anthropic Messages wire format. Copilot only serves
|
|
285
|
+
* Claude models on its native `/v1/messages` endpoint; a gpt/gemini request
|
|
286
|
+
* sent there 400s. This classifier decides, from the RESOLVED model id and its
|
|
287
|
+
* catalog metadata, whether a request stays on the native passthrough
|
|
288
|
+
* (`createMessages`) or is diverted to the translation shim.
|
|
289
|
+
*
|
|
290
|
+
* Non-regression is the whole point — every Claude model (opus / sonnet / haiku /
|
|
291
|
+
* any `claude-*` or Anthropic-vendored id) MUST return "claude-passthrough" so
|
|
292
|
+
* its bytes reach `createMessages` unchanged — even if future catalog metadata
|
|
293
|
+
* were to (wrongly) advertise a `/responses` endpoint for it. The Claude check
|
|
294
|
+
* is therefore keyed off identity (id / vendor / family), NOT the endpoint.
|
|
295
|
+
*
|
|
296
|
+
* Non-Claude models are diverted to the translation shim by the endpoint the
|
|
297
|
+
* catalog says they serve: `/responses` models (gpt-5.5, gpt-5.3-codex, …) take
|
|
298
|
+
* the Responses path (`responses-shim`), `/chat/completions` models (gemini,
|
|
299
|
+
* and any chat-default model) take the chat path (`chat-shim`). The decision is
|
|
300
|
+
* derived from `pickEndpoint` (catalog `supported_endpoints`), never a
|
|
301
|
+
* hardcoded slug list, so it generalizes. Copilot only serves Claude models on
|
|
302
|
+
* its native `/v1/messages`, so diverting every non-Claude model to a shim is
|
|
303
|
+
* correct — a non-Claude request sent to `/v1/messages` would 400.
|
|
304
|
+
*/
|
|
305
|
+
/**
|
|
306
|
+
* Match "claude"/"anthropic" as a delimiter-bounded segment anywhere in a model
|
|
307
|
+
* id: at the start, or after a `/ _ . : -` path/version delimiter, and followed
|
|
308
|
+
* by end-of-string, another such delimiter, or a digit. This catches catalog
|
|
309
|
+
* aliases like `github/claude-3-7-sonnet` or `anthropic/…` whose vendor is
|
|
310
|
+
* "github" and whose family is empty — where the token only surfaces mid-id —
|
|
311
|
+
* while NOT firing on incidental substrings like `notclaude`. Deliberately
|
|
312
|
+
* over-inclusive at the boundaries: we fail CLOSED toward Claude (→ passthrough)
|
|
313
|
+
* so a real Claude model can never be diverted to the non-Claude shim.
|
|
314
|
+
*/
|
|
315
|
+
const CLAUDE_ID_RE = /(^|[/_.:-])(claude|anthropic)(?=$|[/_.:-]|\d)/i;
|
|
316
|
+
/**
|
|
317
|
+
* True when the target is a Claude / Anthropic model. Matches on any of:
|
|
318
|
+
* catalog vendor containing "anthropic", capability family containing "claude",
|
|
319
|
+
* or a Claude/anthropic path-segment (see `CLAUDE_ID_RE`) in ANY id we hold —
|
|
320
|
+
* the resolved id (`modelId`), the pre-resolution request id (`originalModelId`),
|
|
321
|
+
* or the catalog entry's own id (`model.id`). Conservative by design: when in
|
|
322
|
+
* doubt it returns true so a Claude request can never be diverted to the shim.
|
|
323
|
+
*/
|
|
324
|
+
function isClaudeModel(modelId, model, originalModelId) {
|
|
325
|
+
if (model) {
|
|
326
|
+
if ((model.vendor?.toLowerCase() ?? "").includes("anthropic")) return true;
|
|
327
|
+
if ((model.capabilities?.family?.toLowerCase() ?? "").includes("claude")) return true;
|
|
328
|
+
}
|
|
329
|
+
return [
|
|
330
|
+
modelId,
|
|
331
|
+
originalModelId,
|
|
332
|
+
model?.id
|
|
333
|
+
].some((id) => typeof id === "string" && CLAUDE_ID_RE.test(id));
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Decide the route for a resolved model id + its catalog entry.
|
|
337
|
+
*
|
|
338
|
+
* - No model id, or a Claude model → "claude-passthrough" (existing behaviour).
|
|
339
|
+
* - A non-Claude model whose catalog endpoint is `/responses` → "responses-shim".
|
|
340
|
+
* - A non-Claude model whose catalog endpoint is `/chat/completions` (gemini and
|
|
341
|
+
* any chat-default model) → "chat-shim".
|
|
342
|
+
* - A non-Claude model absent from the catalog (so we can't confirm an endpoint)
|
|
343
|
+
* → "claude-passthrough" (unchanged; we don't divert what we can't classify).
|
|
344
|
+
* - A non-Claude model that IS in the catalog and serves NEITHER of the two shim
|
|
345
|
+
* endpoints (`pickEndpoint` → undefined) → "claude-passthrough" as well.
|
|
346
|
+
*
|
|
347
|
+
* Those last two land on the same route but are NOT the same answer, and the
|
|
348
|
+
* coincidence is deliberate rather than a collapsed default (contrast
|
|
349
|
+
* `resolveEndpointForModelId`, whose callers must tell them apart because
|
|
350
|
+
* guessing there produces an opaque upstream 400). Here neither shim is even a
|
|
351
|
+
* candidate: a shim can only speak `/responses` or `/chat/completions`, so
|
|
352
|
+
* diverting a model that serves neither would 400 just as surely. Passthrough
|
|
353
|
+
* is the better default because it is sometimes RIGHT — a non-Claude catalog
|
|
354
|
+
* model advertising `/v1/messages` is served by exactly the endpoint
|
|
355
|
+
* passthrough uses. It also preserves this module's fail-CLOSED-toward-Claude
|
|
356
|
+
* invariant: an unclassifiable model is never diverted.
|
|
357
|
+
*
|
|
358
|
+
* KNOWN GAP (audited, deliberately not fixed here): a non-Claude model serving
|
|
359
|
+
* only something we cannot speak at all (say `/embeddings`) also lands on
|
|
360
|
+
* passthrough and will still 400 upstream — `logEndpointMismatch(modelId,
|
|
361
|
+
* "/v1/messages")` logs it at the passthrough seam, but no local error is
|
|
362
|
+
* raised. Closing that needs a change in `src/routes/messages/handler.ts`,
|
|
363
|
+
* which this seam does not own. It is strictly narrower than the defect fixed
|
|
364
|
+
* in `resolveEndpointForModelId`: no such model is reachable as a Claude Code
|
|
365
|
+
* `/v1/messages` target today, whereas the plan worker's `claude-opus-5`
|
|
366
|
+
* default is.
|
|
367
|
+
*
|
|
368
|
+
* `originalModelId` is the optional pre-resolution request id; when supplied it
|
|
369
|
+
* is checked for Claude-likeness alongside the resolved id so an alias that
|
|
370
|
+
* resolves to a non-Claude-looking id can't slip past.
|
|
371
|
+
*/
|
|
372
|
+
function classifyMessagesRoute(modelId, model, originalModelId) {
|
|
373
|
+
if (!modelId) return "claude-passthrough";
|
|
374
|
+
if (isClaudeModel(modelId, model, originalModelId)) return "claude-passthrough";
|
|
375
|
+
if (!model) return "claude-passthrough";
|
|
376
|
+
const endpoint = pickEndpoint(model);
|
|
377
|
+
if (endpoint === "responses") return "responses-shim";
|
|
378
|
+
if (endpoint === "chat") return "chat-shim";
|
|
379
|
+
return "claude-passthrough";
|
|
380
|
+
}
|
|
166
381
|
//#endregion
|
|
167
382
|
//#region src/lib/port.ts
|
|
168
383
|
const DEFAULT_PORT = 8787;
|
|
@@ -209,11 +424,22 @@ const DEFAULT_CLAUDE_MODEL_FALLBACKS = [
|
|
|
209
424
|
* This helper detects the catalog state at launch and only opts in
|
|
210
425
|
* when the backend can actually serve 1M.
|
|
211
426
|
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* `
|
|
216
|
-
*
|
|
427
|
+
* This helper answers the question only for the OPUS families, because a
|
|
428
|
+
* family is what it is asked about (`-m 4.7` names no slug). Every other lead
|
|
429
|
+
* slug — `-m fast`, a full slug a power user pins, the implicit budget lead —
|
|
430
|
+
* goes through `withOneMSuffixForLead` (`./one-m-context`) instead, which
|
|
431
|
+
* resolves the slug first and then reads the resolved entry's advertised
|
|
432
|
+
* window. The two agree wherever both can be asked: a family that resolves to a
|
|
433
|
+
* 1M backend is 1M by either route.
|
|
434
|
+
*
|
|
435
|
+
* A previous revision of this comment claimed Sonnet and Haiku were left bare
|
|
436
|
+
* because "Copilot has no 1M backend for them". That was true when it was
|
|
437
|
+
* written and is now false for Sonnet: the live catalog advertises
|
|
438
|
+
* `max_context_window_tokens: 1_000_000` on both `claude-sonnet-5` and
|
|
439
|
+
* `claude-sonnet-4.6` (Haiku 4.5 really is 200K, and is left bare by the same
|
|
440
|
+
* catalog check rather than by a hardcoded family rule). Nothing here is
|
|
441
|
+
* family-gated any more — the catalog decides per model, so the next family
|
|
442
|
+
* that ships 1M is picked up without an edit.
|
|
217
443
|
*
|
|
218
444
|
* Must be called AFTER `cacheModels()` has populated `state.models`.
|
|
219
445
|
* Returns the bare slug if the catalog isn't populated (resolveModel
|
|
@@ -221,6 +447,88 @@ const DEFAULT_CLAUDE_MODEL_FALLBACKS = [
|
|
|
221
447
|
* variant" — defaulting safe-side preserves the pre-change behavior).
|
|
222
448
|
*/
|
|
223
449
|
const DEFAULT_OPUS_FAMILY = "5";
|
|
450
|
+
/** The lead `-m fast` selects. Anthropic-published dashed slug that is also the
|
|
451
|
+
* Copilot catalog id verbatim, so `resolveModel` matches it exactly. */
|
|
452
|
+
const BUDGET_LEAD_MODEL = "claude-sonnet-5";
|
|
453
|
+
/** Small/fast tier for a budget lead, in the two forms this codebase needs.
|
|
454
|
+
*
|
|
455
|
+
* `SLUG` is the Anthropic-published DASHED form and is what goes into
|
|
456
|
+
* `ANTHROPIC_SMALL_FAST_MODEL` / `ANTHROPIC_DEFAULT_HAIKU_MODEL`: Claude Code's
|
|
457
|
+
* `/model` registry is keyed on Anthropic slugs, and seeding Copilot's dotted
|
|
458
|
+
* id there reproduces the documented `claude-opus-5` failure where the picker
|
|
459
|
+
* silently falls back to an older model. `CATALOG_ID` is Copilot's DOTTED id
|
|
460
|
+
* and is what the presence probe must test, because that is the id the catalog
|
|
461
|
+
* actually carries. `resolveModel` bridges the two at request time. */
|
|
462
|
+
const BUDGET_SMALL_FAST_SLUG = "claude-haiku-4-5";
|
|
463
|
+
const BUDGET_SMALL_FAST_CATALOG_ID = "claude-haiku-4.5";
|
|
464
|
+
/**
|
|
465
|
+
* Resolve the `-m` argument to the lead slug to launch with.
|
|
466
|
+
*
|
|
467
|
+
* - `fast` → `BUDGET_LEAD_MODEL` (budget mode)
|
|
468
|
+
* - `N.M` → the best variant of that Opus family, via `pickClaudeDefault`
|
|
469
|
+
* - a full slug → unchanged, including Copilot slugs a power user pins
|
|
470
|
+
* - absent → the ordinary default
|
|
471
|
+
*
|
|
472
|
+
* Every branch is `[1m]`-decorated against the live catalog, by
|
|
473
|
+
* `pickClaudeDefault` on the two Opus-family branches and by
|
|
474
|
+
* `withOneMSuffixForLead` on the other two. Pinning a MODEL is not a request to
|
|
475
|
+
* give up four fifths of its context window, which is what leaving the other
|
|
476
|
+
* two bare amounted to: `claude-sonnet-5` advertises a 1M window, so `-m fast`
|
|
477
|
+
* and `-m claude-sonnet-5` were both budgeted locally at Claude Code's 200K
|
|
478
|
+
* default and auto-compacted at roughly a fifth of the real window. The
|
|
479
|
+
* decoration is catalog-gated per model, so a genuinely 200K model
|
|
480
|
+
* (`claude-haiku-4.5`) still comes back bare.
|
|
481
|
+
*
|
|
482
|
+
* `fast` resolves to an ordinary slug rather than setting a mode flag, because
|
|
483
|
+
* budget mode is keyed off the RESOLVED lead everywhere it matters (the advisor
|
|
484
|
+
* escalation, the delegation prose, the small/fast tier). `-m fast` and
|
|
485
|
+
* `-m claude-sonnet-5` must therefore produce identical sessions, which a flag
|
|
486
|
+
* only one of the two set would break. The shared decoration is part of that
|
|
487
|
+
* identity: decorating one branch and not the other would reintroduce the
|
|
488
|
+
* divergence through the context budget instead of through a flag.
|
|
489
|
+
*
|
|
490
|
+
* Callers must keep treating any explicit `-m` as explicit: the
|
|
491
|
+
* `DEFAULT_CLAUDE_MODEL_FALLBACKS` walk applies to the implicit-default path
|
|
492
|
+
* only, so it cannot override a requested family (or `fast`) with an older Opus.
|
|
493
|
+
*/
|
|
494
|
+
function resolveLeadSlugArg(modelArg) {
|
|
495
|
+
const arg = modelArg?.trim();
|
|
496
|
+
if (!arg) return pickClaudeDefault();
|
|
497
|
+
if (arg.toLowerCase() === "fast") return withOneMSuffixForLead(BUDGET_LEAD_MODEL);
|
|
498
|
+
const opusFamilyShorthand = arg.match(/^(\d+\.\d+)$/)?.[1];
|
|
499
|
+
if (opusFamilyShorthand) return pickClaudeDefault(opusFamilyShorthand);
|
|
500
|
+
return withOneMSuffixForLead(arg);
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* True when `slug` names a Claude model that is NOT an Opus tier — the
|
|
504
|
+
* "budget lead" condition.
|
|
505
|
+
*
|
|
506
|
+
* Selecting sonnet or haiku as the lead is a decision to spend less while
|
|
507
|
+
* holding quality as far as possible, and three surfaces key off it: the
|
|
508
|
+
* advisor escalates to the Anthropic frontier (`resolveAdvisorModel`), the
|
|
509
|
+
* injected delegation prose puts the cheap agent tiers first
|
|
510
|
+
* (`buildNativeReachClauses`), and the small/fast tier drops to Haiku
|
|
511
|
+
* (`getClaudeCodeEnvVars`). One definition here so those three cannot disagree
|
|
512
|
+
* about what counts as a budget lead.
|
|
513
|
+
*
|
|
514
|
+
* Resolves before the family test so the Anthropic dashed form, Copilot's
|
|
515
|
+
* dotted form, and `pickClaudeDefault`'s literal `[1m]` suffix all classify
|
|
516
|
+
* alike. A non-Claude lead is not a budget lead: the concept is about picking a
|
|
517
|
+
* lighter tier WITHIN the Claude family, and the gpt/gemini shim models have
|
|
518
|
+
* their own cost profile that this switch says nothing about.
|
|
519
|
+
*
|
|
520
|
+
* CONTRACT: `slug` is an already-resolved LEAD SLUG, never a raw `-m` argument.
|
|
521
|
+
* `"fast"` and the `N.M` shorthand are not Claude slugs and would classify
|
|
522
|
+
* false here; run them through `resolveLeadSlugArg` first, which is what every
|
|
523
|
+
* caller does. Resolving internally instead would drag `pickClaudeDefault`'s
|
|
524
|
+
* catalog dependency into a pure predicate and make the same input answer
|
|
525
|
+
* differently before and after the catalog loads.
|
|
526
|
+
*/
|
|
527
|
+
function isBudgetClaudeLead(slug) {
|
|
528
|
+
if (!slug) return false;
|
|
529
|
+
if (!isClaudeModel(slug)) return false;
|
|
530
|
+
return !/opus/i.test(resolveModel(slug));
|
|
531
|
+
}
|
|
224
532
|
function pickClaudeDefault(opusFamily = DEFAULT_OPUS_FAMILY) {
|
|
225
533
|
const dotted = opusFamily.replace(/-/g, ".");
|
|
226
534
|
const bareSlug = `claude-opus-${dotted.replace(/\./g, "-")}`;
|
|
@@ -19985,70 +20293,6 @@ function detectAgentCall(input) {
|
|
|
19985
20293
|
});
|
|
19986
20294
|
}
|
|
19987
20295
|
//#endregion
|
|
19988
|
-
//#region src/services/copilot/endpoint.ts
|
|
19989
|
-
/**
|
|
19990
|
-
* Catalog spellings that mean each of our two clients. Copilot is not
|
|
19991
|
-
* self-consistent about the `/v1` prefix — the live catalog advertises
|
|
19992
|
-
* `/v1/messages` prefixed but `/chat/completions` bare, and this repo's own
|
|
19993
|
-
* fixtures carry both forms — so an exact-match on the bare spelling alone
|
|
19994
|
-
* silently misses a real shape. `src/lib/model-validation.ts` already
|
|
19995
|
-
* normalizes the same way (`ENDPOINT_ALIASES`); this keeps the two agreeing.
|
|
19996
|
-
*
|
|
19997
|
-
* Matching is EXACT against this set, never a suffix/`includes` test: a
|
|
19998
|
-
* `ws:/responses` (websocket transport) entry is NOT the `/responses` HTTP
|
|
19999
|
-
* client and must keep resolving to "serves neither".
|
|
20000
|
-
*/
|
|
20001
|
-
const CHAT_ENDPOINTS = /* @__PURE__ */ new Set(["/chat/completions", "/v1/chat/completions"]);
|
|
20002
|
-
const RESPONSES_ENDPOINTS = /* @__PURE__ */ new Set(["/responses", "/v1/responses"]);
|
|
20003
|
-
/**
|
|
20004
|
-
* Decide which endpoint to call for a model from its catalog
|
|
20005
|
-
* `supported_endpoints`. Prefers `/chat/completions` when available (the
|
|
20006
|
-
* simpler, more widely-supported shape) and falls back to `/responses` for
|
|
20007
|
-
* models that ONLY serve the Responses API — the gpt-5.x family except
|
|
20008
|
-
* `gpt-5-mini` / `gpt-5.4` (e.g. `gpt-5.4-mini`, `gpt-5.5`, the
|
|
20009
|
-
* `*-codex` models). Returns undefined when the model serves neither, so a
|
|
20010
|
-
* caller can skip it rather than 400 on `unsupported_api_for_model`.
|
|
20011
|
-
*
|
|
20012
|
-
* A model that OMITS `supported_endpoints` is treated as chat-eligible: the
|
|
20013
|
-
* catalog historically omits the field for chat-default models, and
|
|
20014
|
-
* excluding those would be a worse regression than the gap this guards.
|
|
20015
|
-
*/
|
|
20016
|
-
function pickEndpoint(model) {
|
|
20017
|
-
const eps = model.supported_endpoints;
|
|
20018
|
-
if (!eps || eps.length === 0) return "chat";
|
|
20019
|
-
if (eps.some((e) => CHAT_ENDPOINTS.has(e))) return "chat";
|
|
20020
|
-
if (eps.some((e) => RESPONSES_ENDPOINTS.has(e))) return "responses";
|
|
20021
|
-
}
|
|
20022
|
-
/**
|
|
20023
|
-
* `pickEndpoint` by model id against the live catalog, WITHOUT collapsing
|
|
20024
|
-
* "absent from the catalog" into "serves neither of our endpoints".
|
|
20025
|
-
*
|
|
20026
|
-
* This function deliberately has no default. The predecessor
|
|
20027
|
-
* (`endpointForModelId`) returned `pickEndpoint(found) ?? "chat"`, which
|
|
20028
|
-
* coerced both cases to "chat" — defensible for an unknown id, silently wrong
|
|
20029
|
-
* for a catalog model serving only, say, `/v1/messages`: the caller would drive
|
|
20030
|
-
* it through the chat client and get an opaque upstream 400 with no local
|
|
20031
|
-
* signal about the real cause. `src/lib/browser-mcp/compressor.ts` already
|
|
20032
|
-
* treats that case correctly (`if (!endpoint) continue`); this makes the same
|
|
20033
|
-
* distinction available to callers that resolve by id.
|
|
20034
|
-
*
|
|
20035
|
-
* Callers that legitimately want the chat default for an unknown id can still
|
|
20036
|
-
* have it — they just have to write it, per case, on purpose.
|
|
20037
|
-
*/
|
|
20038
|
-
function resolveEndpointForModelId(id) {
|
|
20039
|
-
const found = state.models?.data?.find((m) => m.id === id);
|
|
20040
|
-
if (!found) return { kind: "unknown-model" };
|
|
20041
|
-
const endpoint = pickEndpoint(found);
|
|
20042
|
-
if (endpoint) return {
|
|
20043
|
-
kind: "endpoint",
|
|
20044
|
-
endpoint
|
|
20045
|
-
};
|
|
20046
|
-
return {
|
|
20047
|
-
kind: "unreachable",
|
|
20048
|
-
endpoints: found.supported_endpoints ?? []
|
|
20049
|
-
};
|
|
20050
|
-
}
|
|
20051
|
-
//#endregion
|
|
20052
20296
|
//#region src/lib/browser-mcp/compressor.ts
|
|
20053
20297
|
/**
|
|
20054
20298
|
* Static fallback chain for the inner compressor. Order is preference:
|
|
@@ -27627,8 +27871,26 @@ function logTelemetry(t) {
|
|
|
27627
27871
|
function toolAcceptsWorkspace(tool) {
|
|
27628
27872
|
return tool.capability === "worker" || tool.toolNameHttp === "code" || tool.toolNameHttp === "run_workflow";
|
|
27629
27873
|
}
|
|
27874
|
+
/**
|
|
27875
|
+
* Fold the per-session `X-GH-Workspace` header into `args.workspace` when the
|
|
27876
|
+
* caller left it empty, and REPORT which of the two the tool ended up with.
|
|
27877
|
+
*
|
|
27878
|
+
* The return value is the load-bearing part. This function mutates `args`, so
|
|
27879
|
+
* once it has run a header-derived workspace is byte-indistinguishable from one
|
|
27880
|
+
* the caller chose — and those two cases warrant very different treatment. A
|
|
27881
|
+
* caller that named a directory has told us where it is; a header is a
|
|
27882
|
+
* connection-level default that may be stale (it is computed by a helper Claude
|
|
27883
|
+
* Code runs, and the calling agent may since have moved into a git worktree).
|
|
27884
|
+
* `runWorkerToolCall` uses the distinction to decide what to tell the caller
|
|
27885
|
+
* about the tree it actually ran in, so the provenance must survive the merge.
|
|
27886
|
+
*/
|
|
27630
27887
|
function applySessionWorkspace(args, sessionWorkspace, tool) {
|
|
27631
|
-
if (
|
|
27888
|
+
if (args.workspace !== void 0 && args.workspace !== "") return "argument";
|
|
27889
|
+
if ((!tool || toolAcceptsWorkspace(tool)) && typeof sessionWorkspace === "string" && sessionWorkspace.length > 0 && path.isAbsolute(sessionWorkspace)) {
|
|
27890
|
+
args.workspace = sessionWorkspace;
|
|
27891
|
+
return "session";
|
|
27892
|
+
}
|
|
27893
|
+
return "absent";
|
|
27632
27894
|
}
|
|
27633
27895
|
async function handleToolsCall(body, scope, sessionWorkspace) {
|
|
27634
27896
|
const params = body.params ?? {};
|
|
@@ -27662,7 +27924,8 @@ async function handleToolsCall(body, scope, sessionWorkspace) {
|
|
|
27662
27924
|
personaContext = typeof args.context === "string" ? args.context : void 0;
|
|
27663
27925
|
if (args.imagePaths !== void 0) {
|
|
27664
27926
|
if (!Array.isArray(args.imagePaths) || args.imagePaths.some((v) => typeof v !== "string")) return rpcError(body.id, RPC_INVALID_PARAMS, "tools/call: arguments.imagePaths must be an array of strings");
|
|
27665
|
-
const
|
|
27927
|
+
const imageRoot = typeof sessionWorkspace === "string" && sessionWorkspace.length > 0 && path.isAbsolute(sessionWorkspace) ? sessionWorkspace : process.cwd();
|
|
27928
|
+
const loaded = await loadPeerImages(args.imagePaths, imageRoot);
|
|
27666
27929
|
if (!loaded.ok) return rpcError(body.id, RPC_INVALID_PARAMS, `tools/call: ${loaded.error}`);
|
|
27667
27930
|
personaImages = loaded.images;
|
|
27668
27931
|
}
|
|
@@ -27700,8 +27963,8 @@ async function handleToolsCall(body, scope, sessionWorkspace) {
|
|
|
27700
27963
|
const telemetryName = persona ? persona.agentName : nonPersonaTool.toolNameHttp;
|
|
27701
27964
|
const telemetryModel = persona ? persona.model : "(non-persona)";
|
|
27702
27965
|
try {
|
|
27703
|
-
|
|
27704
|
-
const result = persona ? await callPersona(persona, personaPrompt, personaContext, personaEffort, aborter?.signal, personaImages) : await nonPersonaTool.handler(args, aborter?.signal);
|
|
27966
|
+
const workspaceSource = nonPersonaTool ? applySessionWorkspace(args, sessionWorkspace, nonPersonaTool) : "absent";
|
|
27967
|
+
const result = persona ? await callPersona(persona, personaPrompt, personaContext, personaEffort, aborter?.signal, personaImages) : await nonPersonaTool.handler(args, aborter?.signal, { workspaceSource });
|
|
27705
27968
|
logTelemetry({
|
|
27706
27969
|
name: telemetryName,
|
|
27707
27970
|
model: telemetryModel,
|
|
@@ -28027,6 +28290,98 @@ function handleMcpDelete(c) {
|
|
|
28027
28290
|
return c.body(null, 200);
|
|
28028
28291
|
}
|
|
28029
28292
|
//#endregion
|
|
28293
|
+
//#region src/lib/reasoning-effort.ts
|
|
28294
|
+
/**
|
|
28295
|
+
* Reasoning-effort bucketing + clamping shared by the `/v1/messages` handler
|
|
28296
|
+
* (adaptive-thinking translation) and the Anthropic-translation shim
|
|
28297
|
+
* (thinking-budget → Responses `reasoning.effort`).
|
|
28298
|
+
*
|
|
28299
|
+
* Extracted from `src/routes/messages/handler.ts` so a `~/lib/*` module can
|
|
28300
|
+
* depend on it without importing route code (and without forming a
|
|
28301
|
+
* handler → shim → handler import cycle). `handler.ts` re-exports these for
|
|
28302
|
+
* backward compatibility with existing imports/tests.
|
|
28303
|
+
*/
|
|
28304
|
+
/**
|
|
28305
|
+
* Copilot's reasoning-effort tiers, lowest to highest.
|
|
28306
|
+
*
|
|
28307
|
+
* Both ends were added after the fact and both are load-bearing:
|
|
28308
|
+
*
|
|
28309
|
+
* `none` is advertised by every gpt-5.x entry in the live catalog. While it was
|
|
28310
|
+
* missing here it was treated as an UNRECOGNIZED value, so a client asking for
|
|
28311
|
+
* the MINIMUM on a model that does not offer it (gemini advertises only
|
|
28312
|
+
* low/medium/high) was anchored at the unknown-value tier and clamped to
|
|
28313
|
+
* `high` — the maximum. Listing it makes the clamp resolve to `low` instead,
|
|
28314
|
+
* which is what "nearest supported tier" should always have meant.
|
|
28315
|
+
*
|
|
28316
|
+
* `max` is advertised by `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`,
|
|
28317
|
+
* `claude-opus-5` and the 4.7/4.8 Opus lines, and Claude Code's effort picker
|
|
28318
|
+
* offers it for any model whose entry allows it. Listing it lets an explicit
|
|
28319
|
+
* selection pass through, and lets `clampEffort` land on it for a model that
|
|
28320
|
+
* advertises nothing lower.
|
|
28321
|
+
*
|
|
28322
|
+
* `bucketEffort` deliberately reaches neither end — see below.
|
|
28323
|
+
*/
|
|
28324
|
+
const EFFORT_ORDER = [
|
|
28325
|
+
"none",
|
|
28326
|
+
"low",
|
|
28327
|
+
"medium",
|
|
28328
|
+
"high",
|
|
28329
|
+
"xhigh",
|
|
28330
|
+
"max"
|
|
28331
|
+
];
|
|
28332
|
+
/** Anchor for an effort value that is not a recognized tier at all.
|
|
28333
|
+
*
|
|
28334
|
+
* Deliberately NOT the top of `EFFORT_ORDER`. Callers reach this only when the
|
|
28335
|
+
* incoming value is unrecognized, which is a guess — and resolving a guess to
|
|
28336
|
+
* the most expensive tier a model advertises would silently spend more than the
|
|
28337
|
+
* caller could have meant. Anchoring here and clamping DOWN keeps the behavior
|
|
28338
|
+
* identical to before `max` joined the ladder, while `max` stays reachable by
|
|
28339
|
+
* explicit, valid selection. */
|
|
28340
|
+
const UNKNOWN_EFFORT_ANCHOR = "xhigh";
|
|
28341
|
+
/**
|
|
28342
|
+
* Bucket a thinking budget into a Copilot reasoning-effort string.
|
|
28343
|
+
* `<2000`→low, `<8000`→medium, `<24000`→high, else→xhigh.
|
|
28344
|
+
* Defaults missing/non-numeric budgets to 8000 ("high").
|
|
28345
|
+
*
|
|
28346
|
+
* The ceiling stays at `xhigh` even though `max` exists: Anthropic's
|
|
28347
|
+
* `budget_tokens` is unbounded above, so any threshold chosen for a `max`
|
|
28348
|
+
* bucket would silently re-tier existing callers whose budgets already map to
|
|
28349
|
+
* `xhigh`. `max` is reachable only by explicit selection
|
|
28350
|
+
* (`output_config.effort`), which is an unambiguous request rather than an
|
|
28351
|
+
* inference from a token count.
|
|
28352
|
+
*/
|
|
28353
|
+
function bucketEffort(budget) {
|
|
28354
|
+
const n = typeof budget === "number" && Number.isFinite(budget) ? budget : 8e3;
|
|
28355
|
+
if (n < 2e3) return "low";
|
|
28356
|
+
if (n < 8e3) return "medium";
|
|
28357
|
+
if (n < 24e3) return "high";
|
|
28358
|
+
return "xhigh";
|
|
28359
|
+
}
|
|
28360
|
+
/**
|
|
28361
|
+
* Clamp a bucketed effort to the closest value in `supported`. Ties resolve to
|
|
28362
|
+
* the lower-tier option (per EFFORT_ORDER).
|
|
28363
|
+
*
|
|
28364
|
+
* Iterates EFFORT_ORDER (canonical low→xhigh) so the first match on a given
|
|
28365
|
+
* distance is always the lower-tier value, regardless of input order in
|
|
28366
|
+
* `supported`.
|
|
28367
|
+
*/
|
|
28368
|
+
function clampEffort(bucketed, supported) {
|
|
28369
|
+
if (supported.includes(bucketed)) return bucketed;
|
|
28370
|
+
const targetIdx = EFFORT_ORDER.indexOf(bucketed);
|
|
28371
|
+
let best;
|
|
28372
|
+
let bestDist = Infinity;
|
|
28373
|
+
for (let i = 0; i < EFFORT_ORDER.length; i++) {
|
|
28374
|
+
const value = EFFORT_ORDER[i];
|
|
28375
|
+
if (!supported.includes(value)) continue;
|
|
28376
|
+
const dist = Math.abs(i - targetIdx);
|
|
28377
|
+
if (dist < bestDist) {
|
|
28378
|
+
bestDist = dist;
|
|
28379
|
+
best = value;
|
|
28380
|
+
}
|
|
28381
|
+
}
|
|
28382
|
+
return best ?? bucketed;
|
|
28383
|
+
}
|
|
28384
|
+
//#endregion
|
|
28030
28385
|
//#region src/lib/stream-relay.ts
|
|
28031
28386
|
const ENCODER$1 = new TextEncoder();
|
|
28032
28387
|
/**
|
|
@@ -28514,10 +28869,15 @@ function rememberThinkingHistoryRepair(fingerprint) {
|
|
|
28514
28869
|
* re-call Copilot for the next turn — stream onto the SAME
|
|
28515
28870
|
* SSE connection (no new message_start; the original one is
|
|
28516
28871
|
* still open). Loop up to ADVISOR_MAX_TURNS times.
|
|
28517
|
-
* 4.
|
|
28518
|
-
* family than the main loop (gpt-5.6-sol
|
|
28519
|
-
*
|
|
28520
|
-
*
|
|
28872
|
+
* 4. Lead-aware model choice: route the advisor call to a different model
|
|
28873
|
+
* family than the main loop (gpt-5.6-sol) so the user gets a true "second
|
|
28874
|
+
* set of eyes" instead of Opus reviewing Opus (gemini-critic finding). When
|
|
28875
|
+
* the LEAD is a lighter Claude tier the choice inverts and the advisor
|
|
28876
|
+
* escalates to `ADVISOR_ESCALATION_MODEL` instead — see that constant for
|
|
28877
|
+
* why trading the cross-lab property is the right call on that path.
|
|
28878
|
+
* 5. Effort follows the Claude Code effort picker (`resolveAdvisorEffort`)
|
|
28879
|
+
* rather than a hardcoded constant, floored so a low picker cannot render
|
|
28880
|
+
* the consultation useless.
|
|
28521
28881
|
*
|
|
28522
28882
|
* The translate-loop is bounded to a single user request — no
|
|
28523
28883
|
* persistent state across requests is needed (unlike Phase G's
|
|
@@ -28541,6 +28901,185 @@ const ADVISOR_CLIENT_TOOL_NAME = "advisor";
|
|
|
28541
28901
|
* model — Opus 4.6/Sonnet 4.6 typically). */
|
|
28542
28902
|
const ADVISOR_DEFAULT_MODEL = "gpt-5.6-sol";
|
|
28543
28903
|
const ADVISOR_DEFAULT_EFFORT = "xhigh";
|
|
28904
|
+
/** The Anthropic frontier model the advisor escalates to when the LEAD is a
|
|
28905
|
+
* lighter Claude tier (sonnet, haiku).
|
|
28906
|
+
*
|
|
28907
|
+
* Selecting a lighter lead is a decision to work on a budget while holding
|
|
28908
|
+
* quality: the lead does the legwork and escalates for direction. Without this,
|
|
28909
|
+
* a budget lead has no transcript-aware path to the strongest Anthropic
|
|
28910
|
+
* reasoner at all — `opus_critic` is stateless and sees one artifact, and the
|
|
28911
|
+
* `plan` worker is read-only and never sees the transcript.
|
|
28912
|
+
*
|
|
28913
|
+
* This deliberately trades the advisor's cross-lab property on that path. The
|
|
28914
|
+
* advisor is not this repo's review instrument: it catches drift and momentum
|
|
28915
|
+
* and inherits the lead's framing by design, while the fresh-context critics
|
|
28916
|
+
* (`codex_critic`, `gemini_critic`, `codex_reviewer`, `gemini_reviewer`) are
|
|
28917
|
+
* the decorrelation instrument and are untouched. `GH_ROUTER_ADVISOR_MODEL`
|
|
28918
|
+
* keeps a cross-lab advisor one env var away for anyone who wants it back. */
|
|
28919
|
+
const ADVISOR_ESCALATION_MODEL = "claude-opus-5";
|
|
28920
|
+
/** Floor for the advisor's reasoning effort.
|
|
28921
|
+
*
|
|
28922
|
+
* The advisor follows the Claude Code effort picker (see `resolveAdvisorEffort`)
|
|
28923
|
+
* so dialing the picker down makes it cheaper, but it does NOT follow it all the
|
|
28924
|
+
* way to the bottom of `EFFORT_ORDER`. The advisor fires a handful of times per
|
|
28925
|
+
* session (`ADVISOR_MAX_TURNS`, typically 1-3), so it is not the budget line —
|
|
28926
|
+
* the lead's own turns are — while an advisor reasoning at `none`/`low` cannot
|
|
28927
|
+
* do the job the consultation exists for. The picker therefore governs the
|
|
28928
|
+
* `high..max` range. */
|
|
28929
|
+
const ADVISOR_MIN_EFFORT = "high";
|
|
28930
|
+
/** Output cap for the Anthropic-branch advisor call when the catalog carries no
|
|
28931
|
+
* limits for the resolved model. The value the branch used unconditionally
|
|
28932
|
+
* before it became reachable, kept so a catalog-less path is no worse off. */
|
|
28933
|
+
const ADVISOR_FALLBACK_MAX_OUTPUT_TOKENS = 4096;
|
|
28934
|
+
/** Catalog spellings that mean the Responses API. Copilot is inconsistent about
|
|
28935
|
+
* the `/v1` prefix, so both are matched — mirroring `CHAT_ENDPOINTS` /
|
|
28936
|
+
* `RESPONSES_ENDPOINTS` in `src/services/copilot/endpoint.ts`. */
|
|
28937
|
+
const ADVISOR_RESPONSES_ENDPOINTS = /* @__PURE__ */ new Set(["/responses", "/v1/responses"]);
|
|
28938
|
+
/**
|
|
28939
|
+
* Which transport the advisor dispatches on: `/responses` (with
|
|
28940
|
+
* `reasoning.effort`) or `/v1/messages`.
|
|
28941
|
+
*
|
|
28942
|
+
* Catalog-first, name-regex second, and BOTH tests run against the bare id as
|
|
28943
|
+
* well as the given one. `pickEndpoint` is deliberately not reused: it answers
|
|
28944
|
+
* "chat or responses" for the two tool-calling clients and would send
|
|
28945
|
+
* `claude-opus-5` — which advertises `/v1/messages` AND `/chat/completions` — to
|
|
28946
|
+
* chat. The advisor's question is narrower: does this model serve `/responses`?
|
|
28947
|
+
*
|
|
28948
|
+
* The bare-id fallback is what makes `GH_ROUTER_ADVISOR_MODEL` safe. That pin is
|
|
28949
|
+
* accepted verbatim, so an operator can write a vendor-namespaced value like
|
|
28950
|
+
* `openai/gpt-5.6-sol`. Such an id is in no catalog and fails the start-anchored
|
|
28951
|
+
* name regex, so a catalog-only fix still posted it to `/v1/messages` and 400'd
|
|
28952
|
+
* — exported and directly tested for that exact input, because an earlier
|
|
28953
|
+
* version of this function claimed to handle it and did not.
|
|
28954
|
+
*/
|
|
28955
|
+
function advisorUsesResponses(resolvedAdvisorModel) {
|
|
28956
|
+
const bare = resolvedAdvisorModel.slice(resolvedAdvisorModel.lastIndexOf("/") + 1);
|
|
28957
|
+
const endpoints = (state.models?.data?.find((m) => m.id === resolvedAdvisorModel || m.id === bare))?.supported_endpoints;
|
|
28958
|
+
if (endpoints && endpoints.length > 0) return endpoints.some((e) => ADVISOR_RESPONSES_ENDPOINTS.has(e));
|
|
28959
|
+
return /^(gpt-|o\d|.*codex)/i.test(bare);
|
|
28960
|
+
}
|
|
28961
|
+
/** True when the model advertises a usable reasoning-effort ladder. */
|
|
28962
|
+
function advertisedEffortLadder(resolvedAdvisorModel) {
|
|
28963
|
+
const supported = state.models?.data?.find((m) => m.id === resolvedAdvisorModel)?.capabilities?.supports?.reasoning_effort;
|
|
28964
|
+
return Array.isArray(supported) && supported.length > 0 ? supported : void 0;
|
|
28965
|
+
}
|
|
28966
|
+
/** True when the advisor should escalate to `ADVISOR_ESCALATION_MODEL` for this
|
|
28967
|
+
* lead: a Claude lead that is NOT already an Opus tier, on a catalog that
|
|
28968
|
+
* actually carries the escalation model.
|
|
28969
|
+
*
|
|
28970
|
+
* The catalog probe mirrors `standInToolEnabled`'s: never name a model the
|
|
28971
|
+
* account cannot reach. A non-Claude lead never gets here in practice (the
|
|
28972
|
+
* advisor tool is stripped for those before the request reaches this module),
|
|
28973
|
+
* but the check is explicit rather than assumed.
|
|
28974
|
+
*
|
|
28975
|
+
* The probe compares the BARE constant rather than `resolveModel`-ing it first,
|
|
28976
|
+
* which is deliberate and not an oversight: `claude-opus-5` is a single-segment
|
|
28977
|
+
* slug whose dashed and dotted spellings are identical, so resolution is a
|
|
28978
|
+
* no-op, and `resolveModel` WARNS on an id it cannot find — routing this probe
|
|
28979
|
+
* through it would emit that warning on every advisor request for anyone whose
|
|
28980
|
+
* catalog lacks opus-5, which is exactly the tier this returns false for.
|
|
28981
|
+
* `standInToolEnabled` compares the same id the same way. */
|
|
28982
|
+
function shouldEscalateAdvisor(leadModel) {
|
|
28983
|
+
if (!isBudgetClaudeLead(leadModel)) return false;
|
|
28984
|
+
return state.models?.data?.some((m) => m.id === "claude-opus-5") ?? false;
|
|
28985
|
+
}
|
|
28986
|
+
/**
|
|
28987
|
+
* Pick the advisor model for one request from the LEAD model that request is
|
|
28988
|
+
* running on.
|
|
28989
|
+
*
|
|
28990
|
+
* Resolved per request rather than at launch because the lead changes
|
|
28991
|
+
* mid-session via the `/model` picker; launch-time env plumbing would pin the
|
|
28992
|
+
* advisor to whatever was selected at spawn.
|
|
28993
|
+
*
|
|
28994
|
+
* Precedence:
|
|
28995
|
+
* 1. `GH_ROUTER_ADVISOR_MODEL` (trimmed) — the operator pin, checked first so
|
|
28996
|
+
* it works on every lead.
|
|
28997
|
+
* 2. A lighter Claude lead with the escalation model in the catalog.
|
|
28998
|
+
* 3. `ADVISOR_DEFAULT_MODEL`.
|
|
28999
|
+
*
|
|
29000
|
+
* Step 3 returns the LITERAL constant rather than walking the OpenAI frontier
|
|
29001
|
+
* chain. An Opus lead must resolve to exactly what it resolves to today, and a
|
|
29002
|
+
* frontier walk could yield `gpt-5.5` on a catalog missing `gpt-5.6-sol` —
|
|
29003
|
+
* a silent change to the one path that is required not to move.
|
|
29004
|
+
*/
|
|
29005
|
+
/**
|
|
29006
|
+
* Map an operator pin onto the id the catalog actually carries.
|
|
29007
|
+
*
|
|
29008
|
+
* `GH_ROUTER_ADVISOR_MODEL` is free-form, and the natural thing to write is a
|
|
29009
|
+
* vendor-namespaced id like `openai/gpt-5.6-sol`. Copilot's catalog carries the
|
|
29010
|
+
* bare `gpt-5.6-sol`, so forwarding the namespaced form verbatim gets a 400
|
|
29011
|
+
* `model_not_supported` and the advisor silently degrades to its
|
|
29012
|
+
* "[Advisor unavailable: ...]" fallback — measured, not theorised: choosing the
|
|
29013
|
+
* transport correctly was NOT sufficient, because the id itself was still
|
|
29014
|
+
* wrong on the wire.
|
|
29015
|
+
*
|
|
29016
|
+
* An exact catalog hit wins first, so a real id containing a slash could never
|
|
29017
|
+
* be mangled. Only when the pin is absent from the catalog do we try its last
|
|
29018
|
+
* path segment, and only when THAT is present do we rewrite. A pin that matches
|
|
29019
|
+
* nothing is passed through untouched: the catalog may simply not be loaded
|
|
29020
|
+
* yet, and inventing an id would be worse than letting upstream reject it.
|
|
29021
|
+
*/
|
|
29022
|
+
function normalizeAdvisorPin(pinned) {
|
|
29023
|
+
const models = state.models?.data;
|
|
29024
|
+
if (!models) return pinned;
|
|
29025
|
+
if (models.some((m) => m.id === pinned)) return pinned;
|
|
29026
|
+
const bare = pinned.slice(pinned.lastIndexOf("/") + 1);
|
|
29027
|
+
return bare !== pinned && models.some((m) => m.id === bare) ? bare : pinned;
|
|
29028
|
+
}
|
|
29029
|
+
function resolveAdvisorModel(leadModel) {
|
|
29030
|
+
const pinned = process.env.GH_ROUTER_ADVISOR_MODEL?.trim();
|
|
29031
|
+
if (pinned) return {
|
|
29032
|
+
model: normalizeAdvisorPin(pinned),
|
|
29033
|
+
escalated: false
|
|
29034
|
+
};
|
|
29035
|
+
if (leadModel && shouldEscalateAdvisor(leadModel)) return {
|
|
29036
|
+
model: ADVISOR_ESCALATION_MODEL,
|
|
29037
|
+
escalated: true
|
|
29038
|
+
};
|
|
29039
|
+
return {
|
|
29040
|
+
model: ADVISOR_DEFAULT_MODEL,
|
|
29041
|
+
escalated: false
|
|
29042
|
+
};
|
|
29043
|
+
}
|
|
29044
|
+
/**
|
|
29045
|
+
* Resolve the advisor's reasoning effort from the ORIGINAL request body, so the
|
|
29046
|
+
* advisor thinks at the level selected in the Claude Code effort picker instead
|
|
29047
|
+
* of a hardcoded constant.
|
|
29048
|
+
*
|
|
29049
|
+
* The source is the RAW pre-`resolveModelInBody` body, deliberately. By the time
|
|
29050
|
+
* the handler holds a parsed body, `translateThinking` has already bucketed
|
|
29051
|
+
* `thinking.budget_tokens` into `output_config.effort` AND clamped it to the
|
|
29052
|
+
* LEAD model's allowlist — so that value encodes "what the lead could do", not
|
|
29053
|
+
* "what the user picked". Re-clamping it against the advisor cannot recover the
|
|
29054
|
+
* difference: a `max` pick on a lead whose ceiling is `high` would reach an
|
|
29055
|
+
* xhigh-capable advisor as `high`.
|
|
29056
|
+
*
|
|
29057
|
+
* Precedence mirrors the repo-wide rule that an explicit client effort wins:
|
|
29058
|
+
* 1. `output_config.effort`
|
|
29059
|
+
* 2. `bucketEffort(thinking.budget_tokens)`
|
|
29060
|
+
* 3. `ADVISOR_DEFAULT_EFFORT` — a request expressing no preference behaves
|
|
29061
|
+
* exactly as it did before the picker was honored at all.
|
|
29062
|
+
*
|
|
29063
|
+
* Then floor, THEN clamp. The order is load-bearing: a model whose ceiling sits
|
|
29064
|
+
* below `ADVISOR_MIN_EFFORT` must still receive a value it accepts, so the clamp
|
|
29065
|
+
* is allowed to pull back under the floor. Flipping the two would forward an
|
|
29066
|
+
* effort upstream rejects.
|
|
29067
|
+
*/
|
|
29068
|
+
function resolveAdvisorEffort(rawRequestBody, advisorModel) {
|
|
29069
|
+
let requested = ADVISOR_DEFAULT_EFFORT;
|
|
29070
|
+
if (rawRequestBody) try {
|
|
29071
|
+
const body = JSON.parse(rawRequestBody);
|
|
29072
|
+
const oc = body.output_config;
|
|
29073
|
+
const explicit = oc && typeof oc === "object" ? oc.effort : void 0;
|
|
29074
|
+
const thinking = body.thinking;
|
|
29075
|
+
if (typeof explicit === "string" && EFFORT_ORDER.includes(explicit)) requested = explicit;
|
|
29076
|
+
else if (thinking && typeof thinking === "object" && thinking.type === "enabled") requested = bucketEffort(thinking.budget_tokens);
|
|
29077
|
+
} catch {}
|
|
29078
|
+
const floored = EFFORT_ORDER.indexOf(requested) < EFFORT_ORDER.indexOf(ADVISOR_MIN_EFFORT) ? ADVISOR_MIN_EFFORT : requested;
|
|
29079
|
+
const supported = state.models?.data?.find((m) => m.id === resolveModel(advisorModel))?.capabilities?.supports?.reasoning_effort;
|
|
29080
|
+
if (!Array.isArray(supported) || supported.length === 0) return floored;
|
|
29081
|
+
return clampEffort(floored, supported);
|
|
29082
|
+
}
|
|
28544
29083
|
/** ADVISOR_TOOL_INSTRUCTIONS verbatim from cc-backup
|
|
28545
29084
|
* src/utils/advisor.ts — describes when the model should invoke
|
|
28546
29085
|
* the advisor. Long-form prose; see source for justification. */
|
|
@@ -28636,8 +29175,12 @@ const ADVISOR_FALLBACK_MAX_TOKENS = 24e4;
|
|
|
28636
29175
|
* our o200k count and Copilot's full-payload count. The transcript token
|
|
28637
29176
|
* budget is `max_prompt_tokens - reserve`. Generous on purpose: a 400
|
|
28638
29177
|
* `model_max_prompt_tokens_exceeded` degrades to a silent advisor
|
|
28639
|
-
* fallback, and the
|
|
28640
|
-
* gpt-5.6-sol
|
|
29178
|
+
* fallback, and the window given up is marginal against either advisor
|
|
29179
|
+
* model's real prompt window (`claude-opus-5` 936k, `gpt-5.6-sol` ~1M off
|
|
29180
|
+
* the live catalog). Sized as a fraction of the smaller of the two, not as
|
|
29181
|
+
* "irrelevant next to ~1M" — that framing assumed the advisor was always
|
|
29182
|
+
* the cheap side of the pair, which stopped being true once a budget lead
|
|
29183
|
+
* escalates to Opus. */
|
|
28641
29184
|
const ADVISOR_PROMPT_TOKEN_RESERVE = 8e3;
|
|
28642
29185
|
/**
|
|
28643
29186
|
* Derive the TOKEN budget for the rendered transcript from the advisor
|
|
@@ -28748,9 +29291,9 @@ function truncateTailToUnits(text, maxUnits, measure) {
|
|
|
28748
29291
|
* Anthropic's own ADVISOR ("see the whole task + every tool call +
|
|
28749
29292
|
* every result").
|
|
28750
29293
|
*/
|
|
28751
|
-
async function runAdvisor(conversation, advisorModel, advisorEffort, signal) {
|
|
29294
|
+
async function runAdvisor(conversation, advisorModel, advisorEffort, signal, advisorEscalated = false) {
|
|
28752
29295
|
if (signal?.aborted) throw new Error("advisor call aborted before dispatch");
|
|
28753
|
-
const advisorSystem = "You are an expert advisor reviewing an in-progress Claude Code session. The transcript below is the work-in-progress (turns numbered, with tool calls and results inlined). Read carefully and provide concrete, actionable advice on the next step or course-correction. Be specific — cite the parts of the transcript you're responding to. If the assistant is on the right track, say so explicitly. If they're stuck or off-track, name the specific assumption or step to revisit. Aim for 2-5 paragraphs of substantive guidance.";
|
|
29296
|
+
const advisorSystem = "You are an expert advisor reviewing an in-progress Claude Code session. The transcript below is the work-in-progress (turns numbered, with tool calls and results inlined). Read carefully and provide concrete, actionable advice on the next step or course-correction. Be specific — cite the parts of the transcript you're responding to. If the assistant is on the right track, say so explicitly. If they're stuck or off-track, name the specific assumption or step to revisit. Aim for 2-5 paragraphs of substantive guidance." + (advisorEscalated ? " The requesting agent is running a lighter, faster model than you. Give a directive recommendation and commit to the decision rather than laying out options for it to weigh." : "");
|
|
28754
29297
|
const resolvedAdvisorModel = resolveModel(advisorModel);
|
|
28755
29298
|
let measure;
|
|
28756
29299
|
let maxUnits;
|
|
@@ -28765,7 +29308,7 @@ async function runAdvisor(conversation, advisorModel, advisorEffort, signal) {
|
|
|
28765
29308
|
maxUnits = ADVISOR_MAX_CONVERSATION_CHARS;
|
|
28766
29309
|
}
|
|
28767
29310
|
const conversationText = renderConversationAsText(conversation, maxUnits, measure);
|
|
28768
|
-
if (
|
|
29311
|
+
if (advisorUsesResponses(resolvedAdvisorModel)) {
|
|
28769
29312
|
const payload = {
|
|
28770
29313
|
model: resolvedAdvisorModel,
|
|
28771
29314
|
instructions: advisorSystem,
|
|
@@ -28800,15 +29343,22 @@ async function runAdvisor(conversation, advisorModel, advisorEffort, signal) {
|
|
|
28800
29343
|
if (!text) throw new Error(`Advisor model ${resolvedAdvisorModel} returned empty assistant output`);
|
|
28801
29344
|
return text;
|
|
28802
29345
|
}
|
|
29346
|
+
const advisorEntry = state.models?.data?.find((m) => m.id === resolvedAdvisorModel);
|
|
29347
|
+
const limits = advisorEntry?.capabilities?.limits;
|
|
29348
|
+
const maxTokens = limits?.max_non_streaming_output_tokens ?? limits?.max_output_tokens ?? ADVISOR_FALLBACK_MAX_OUTPUT_TOKENS;
|
|
28803
29349
|
const advisorBody = JSON.stringify({
|
|
28804
29350
|
model: resolvedAdvisorModel,
|
|
28805
|
-
max_tokens:
|
|
29351
|
+
max_tokens: maxTokens,
|
|
28806
29352
|
system: advisorSystem,
|
|
28807
29353
|
messages: [{
|
|
28808
29354
|
role: "user",
|
|
28809
29355
|
content: conversationText
|
|
28810
29356
|
}],
|
|
28811
|
-
stream: false
|
|
29357
|
+
stream: false,
|
|
29358
|
+
...advisorEntry?.capabilities?.supports?.adaptive_thinking ? {
|
|
29359
|
+
thinking: { type: "adaptive" },
|
|
29360
|
+
...advertisedEffortLadder(resolvedAdvisorModel) ? { output_config: { effort: advisorEffort } } : {}
|
|
29361
|
+
} : {}
|
|
28812
29362
|
});
|
|
28813
29363
|
const json = await (await withTransientRetry(() => createMessages(advisorBody, {}, signal), {
|
|
28814
29364
|
signal,
|
|
@@ -28863,6 +29413,7 @@ function sseEvent(type, data) {
|
|
|
28863
29413
|
function buildAdvisorStream(opts) {
|
|
28864
29414
|
const advisorModel = opts.advisorModel ?? "gpt-5.6-sol";
|
|
28865
29415
|
const advisorEffort = opts.advisorEffort ?? "xhigh";
|
|
29416
|
+
const advisorEscalated = opts.advisorEscalated ?? false;
|
|
28866
29417
|
const aborter = opts.externalAborter ?? new AbortController();
|
|
28867
29418
|
let conversation = [...opts.initialConversation];
|
|
28868
29419
|
return new ReadableStream({
|
|
@@ -29112,7 +29663,7 @@ function buildAdvisorStream(opts) {
|
|
|
29112
29663
|
const advisorConversation = conversation;
|
|
29113
29664
|
const advisorTexts = await Promise.all(advisorToolUses.map(async () => {
|
|
29114
29665
|
try {
|
|
29115
|
-
return await runAdvisor(advisorConversation, advisorModel, advisorEffort, aborter.signal);
|
|
29666
|
+
return await runAdvisor(advisorConversation, advisorModel, advisorEffort, aborter.signal, advisorEscalated);
|
|
29116
29667
|
} catch (err) {
|
|
29117
29668
|
if (aborter.signal.aborted) throw err;
|
|
29118
29669
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -34568,7 +35119,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
34568
35119
|
},
|
|
34569
35120
|
workspace: {
|
|
34570
35121
|
type: "string",
|
|
34571
|
-
description: "
|
|
35122
|
+
description: "Absolute path of the directory the worker operates in. Pass YOUR OWN current working directory unless you were told to target a different one — the proxy is a separate, long-lived process and cannot see where you are, so it falls back to your session's directory, which is not necessarily yours (a sub-agent or a git worktree moves you without moving the session). Omitting it when the connection supplies nothing is an error rather than a guess. Must be absolute (relative paths rejected)."
|
|
34572
35123
|
},
|
|
34573
35124
|
maxWallClockMs: {
|
|
34574
35125
|
type: "integer",
|
|
@@ -34576,11 +35127,12 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
34576
35127
|
}
|
|
34577
35128
|
}
|
|
34578
35129
|
},
|
|
34579
|
-
async handler(args, signal) {
|
|
35130
|
+
async handler(args, signal, ctx) {
|
|
34580
35131
|
return runWorkerToolCall({
|
|
34581
35132
|
mode: "explore",
|
|
34582
35133
|
args,
|
|
34583
|
-
signal
|
|
35134
|
+
signal,
|
|
35135
|
+
ctx
|
|
34584
35136
|
});
|
|
34585
35137
|
}
|
|
34586
35138
|
},
|
|
@@ -34613,7 +35165,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
34613
35165
|
},
|
|
34614
35166
|
workspace: {
|
|
34615
35167
|
type: "string",
|
|
34616
|
-
description: "
|
|
35168
|
+
description: "Absolute path of the directory the worker operates in. Pass YOUR OWN current working directory unless you were told to target a different one — the proxy is a separate, long-lived process and cannot see where you are, so it falls back to your session's directory, which is not necessarily yours (a sub-agent or a git worktree moves you without moving the session). Omitting it when the connection supplies nothing is an error rather than a guess. Must be absolute (relative paths rejected). Must be inside a git repo (implement always runs in a worktree)."
|
|
34617
35169
|
},
|
|
34618
35170
|
maxWallClockMs: {
|
|
34619
35171
|
type: "integer",
|
|
@@ -34621,11 +35173,12 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
34621
35173
|
}
|
|
34622
35174
|
}
|
|
34623
35175
|
},
|
|
34624
|
-
async handler(args, signal) {
|
|
35176
|
+
async handler(args, signal, ctx) {
|
|
34625
35177
|
return runWorkerToolCall({
|
|
34626
35178
|
mode: "implement",
|
|
34627
35179
|
args,
|
|
34628
|
-
signal
|
|
35180
|
+
signal,
|
|
35181
|
+
ctx
|
|
34629
35182
|
});
|
|
34630
35183
|
}
|
|
34631
35184
|
},
|
|
@@ -34633,7 +35186,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
34633
35186
|
toolNameHttp: "review",
|
|
34634
35187
|
group: "workers",
|
|
34635
35188
|
capability: "worker",
|
|
34636
|
-
description: "Runs as the background `worker-review` agent. Dispatch via the Agent tool (subagent_type: worker-review) so the turn is never blocked; the result arrives as a completion notification.
|
|
35189
|
+
description: "Runs as the background `worker-review` agent. Dispatch via the Agent tool (subagent_type: worker-review) so the turn is never blocked; the result arrives as a completion notification. Code review by an autonomous worker (Pi runtime; default model `gemini-3.1-pro-preview`, default thinking xhigh clamped to high for that model, override via `model` with any Copilot-catalog model that advertises `tool_calls`). It has explore's read-and-search tools PLUS `bash`, so it verifies claims by running them — reproducing a failure or running the build or suite — rather than only reading, and returns severity-ranked findings with `file:line` citations. It gets no edit/write tools unless you pass `worktree: true`, but `bash` runs real commands in the workspace, so a build or test it invokes can touch the tree. Use for reviewing a change, diff, or correctness claim when the reviewer should read the code itself rather than trusting a pasted artifact. Not for architecture critique, implementation, or test authoring; use codex_critic or gemini_critic for design review, implement for edits, and test for independent test creation. If the result is too large to relay it is truncated to a preview and the FULL text is written to a file, whose absolute path is in the returned text; read that file rather than re-running the worker.",
|
|
34637
35190
|
inputSchema: {
|
|
34638
35191
|
type: "object",
|
|
34639
35192
|
required: ["prompt"],
|
|
@@ -34652,9 +35205,13 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
34652
35205
|
enum: WORKER_THINKING_LEVELS,
|
|
34653
35206
|
description: "Optional reasoning depth (defaults to xhigh, clamped to high for the default review model). Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
|
|
34654
35207
|
},
|
|
35208
|
+
worktree: {
|
|
35209
|
+
type: "boolean",
|
|
35210
|
+
description: "Optional. When true, the review runs in an isolated git worktree replaying the workspace's working tree (dirty tracked changes and untracked-not-ignored files), which additionally grants `edit`/`write` so the reviewer can author a throwaway probe test to prove a claim. Default false: the reviewer reads and runs commands in the workspace itself. Prefer the default when verifying needs the build to work — a fresh worktree does not carry IGNORED files, so installed dependencies are absent. Requires a git repository."
|
|
35211
|
+
},
|
|
34655
35212
|
workspace: {
|
|
34656
35213
|
type: "string",
|
|
34657
|
-
description: "
|
|
35214
|
+
description: "Absolute path of the directory the worker operates in. Pass YOUR OWN current working directory unless you were told to target a different one — the proxy is a separate, long-lived process and cannot see where you are, so it falls back to your session's directory, which is not necessarily yours (a sub-agent or a git worktree moves you without moving the session). Omitting it when the connection supplies nothing is an error rather than a guess. Must be absolute (relative paths rejected)."
|
|
34658
35215
|
},
|
|
34659
35216
|
maxWallClockMs: {
|
|
34660
35217
|
type: "integer",
|
|
@@ -34662,11 +35219,12 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
34662
35219
|
}
|
|
34663
35220
|
}
|
|
34664
35221
|
},
|
|
34665
|
-
async handler(args, signal) {
|
|
35222
|
+
async handler(args, signal, ctx) {
|
|
34666
35223
|
return runWorkerToolCall({
|
|
34667
35224
|
mode: "review",
|
|
34668
35225
|
args,
|
|
34669
|
-
signal
|
|
35226
|
+
signal,
|
|
35227
|
+
ctx
|
|
34670
35228
|
});
|
|
34671
35229
|
}
|
|
34672
35230
|
},
|
|
@@ -34695,7 +35253,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
34695
35253
|
},
|
|
34696
35254
|
workspace: {
|
|
34697
35255
|
type: "string",
|
|
34698
|
-
description: "
|
|
35256
|
+
description: "Absolute path of the directory the worker operates in. Pass YOUR OWN current working directory unless you were told to target a different one — the proxy is a separate, long-lived process and cannot see where you are, so it falls back to your session's directory, which is not necessarily yours (a sub-agent or a git worktree moves you without moving the session). Omitting it when the connection supplies nothing is an error rather than a guess. Must be absolute (relative paths rejected)."
|
|
34699
35257
|
},
|
|
34700
35258
|
maxWallClockMs: {
|
|
34701
35259
|
type: "integer",
|
|
@@ -34703,11 +35261,12 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
34703
35261
|
}
|
|
34704
35262
|
}
|
|
34705
35263
|
},
|
|
34706
|
-
async handler(args, signal) {
|
|
35264
|
+
async handler(args, signal, ctx) {
|
|
34707
35265
|
return runWorkerToolCall({
|
|
34708
35266
|
mode: "plan",
|
|
34709
35267
|
args,
|
|
34710
|
-
signal
|
|
35268
|
+
signal,
|
|
35269
|
+
ctx
|
|
34711
35270
|
});
|
|
34712
35271
|
}
|
|
34713
35272
|
},
|
|
@@ -34740,7 +35299,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
34740
35299
|
},
|
|
34741
35300
|
workspace: {
|
|
34742
35301
|
type: "string",
|
|
34743
|
-
description: "
|
|
35302
|
+
description: "Absolute path of the directory the worker operates in. Pass YOUR OWN current working directory unless you were told to target a different one — the proxy is a separate, long-lived process and cannot see where you are, so it falls back to your session's directory, which is not necessarily yours (a sub-agent or a git worktree moves you without moving the session). Omitting it when the connection supplies nothing is an error rather than a guess. Must be absolute (relative paths rejected). Must be inside a git repo (test always runs in a worktree)."
|
|
34744
35303
|
},
|
|
34745
35304
|
maxWallClockMs: {
|
|
34746
35305
|
type: "integer",
|
|
@@ -34748,11 +35307,12 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
|
|
|
34748
35307
|
}
|
|
34749
35308
|
}
|
|
34750
35309
|
},
|
|
34751
|
-
async handler(args, signal) {
|
|
35310
|
+
async handler(args, signal, ctx) {
|
|
34752
35311
|
return runWorkerToolCall({
|
|
34753
35312
|
mode: "test",
|
|
34754
35313
|
args,
|
|
34755
|
-
signal
|
|
35314
|
+
signal,
|
|
35315
|
+
ctx
|
|
34756
35316
|
});
|
|
34757
35317
|
}
|
|
34758
35318
|
},
|
|
@@ -35075,10 +35635,12 @@ function assertMcpToolSurfaceConsistent() {
|
|
|
35075
35635
|
/**
|
|
35076
35636
|
* Shared closure body for the two worker MCP tools. Validates the
|
|
35077
35637
|
* minimal arg shape (prompt required + optional knobs typed), then
|
|
35078
|
-
* forwards to `runWorkerAgent`.
|
|
35079
|
-
*
|
|
35080
|
-
*
|
|
35081
|
-
*
|
|
35638
|
+
* forwards to `runWorkerAgent`. `workspace` comes from the caller's
|
|
35639
|
+
* argument or, failing that, the per-connection session header the
|
|
35640
|
+
* boundary folds in; with neither, the call is REFUSED rather than
|
|
35641
|
+
* defaulted to the proxy's launch cwd (see the resolution block below
|
|
35642
|
+
* for why that default was a bug, and `GH_ROUTER_ALLOW_PROXY_CWD_WORKSPACE`
|
|
35643
|
+
* for the raw-client escape hatch). The engine performs every
|
|
35082
35644
|
* deeper validation (model existence, thinking clamp, worktree
|
|
35083
35645
|
* provisioning, semaphore acquisition, workspace realpath +
|
|
35084
35646
|
* accessibility) and never throws — its `{text, isError?}` envelope
|
|
@@ -35091,7 +35653,7 @@ function assertMcpToolSurfaceConsistent() {
|
|
|
35091
35653
|
* client that ignores the schema.
|
|
35092
35654
|
*/
|
|
35093
35655
|
async function runWorkerToolCall(call) {
|
|
35094
|
-
const { mode, args, signal } = call;
|
|
35656
|
+
const { mode, args, signal, ctx } = call;
|
|
35095
35657
|
const prompt = typeof args.prompt === "string" ? args.prompt : "";
|
|
35096
35658
|
if (!prompt) return {
|
|
35097
35659
|
content: [{
|
|
@@ -35123,7 +35685,7 @@ async function runWorkerToolCall(call) {
|
|
|
35123
35685
|
}
|
|
35124
35686
|
let worktree;
|
|
35125
35687
|
let worktreeNote = "";
|
|
35126
|
-
if (mode === "implement" || mode === "test") {
|
|
35688
|
+
if (mode === "implement" || mode === "test" || mode === "review") {
|
|
35127
35689
|
if (args.worktree !== void 0 && typeof args.worktree !== "boolean") return {
|
|
35128
35690
|
content: [{
|
|
35129
35691
|
type: "text",
|
|
@@ -35131,11 +35693,16 @@ async function runWorkerToolCall(call) {
|
|
|
35131
35693
|
}],
|
|
35132
35694
|
isError: true
|
|
35133
35695
|
};
|
|
35134
|
-
worktree = true;
|
|
35135
|
-
|
|
35696
|
+
if (mode === "review") worktree = args.worktree === true ? true : void 0;
|
|
35697
|
+
else {
|
|
35698
|
+
worktree = true;
|
|
35699
|
+
if (args.worktree === false) worktreeNote = `[note: worker_${mode} always runs in an isolated git worktree; the requested worktree:false was overridden. For in-place edits, use the \`implementer\` subagent.]
|
|
35136
35700
|
|
|
35137
35701
|
`;
|
|
35702
|
+
}
|
|
35138
35703
|
}
|
|
35704
|
+
const allowProxyCwd = process.env.GH_ROUTER_ALLOW_PROXY_CWD_WORKSPACE === "1";
|
|
35705
|
+
const callerGaveWorkspace = args.workspace !== void 0;
|
|
35139
35706
|
let workspace;
|
|
35140
35707
|
if (args.workspace !== void 0) {
|
|
35141
35708
|
if (typeof args.workspace !== "string" || args.workspace.length === 0) return {
|
|
@@ -35160,7 +35727,16 @@ async function runWorkerToolCall(call) {
|
|
|
35160
35727
|
}],
|
|
35161
35728
|
isError: true
|
|
35162
35729
|
};
|
|
35163
|
-
else workspace = process.cwd();
|
|
35730
|
+
else if (allowProxyCwd) workspace = process.cwd();
|
|
35731
|
+
else return {
|
|
35732
|
+
content: [{
|
|
35733
|
+
type: "text",
|
|
35734
|
+
text: `worker_${mode}: a workspace is required. Nothing in this call said which directory to run in, and the proxy's own launch directory is not a safe guess — it is where the proxy was started, which may be a different checkout or git worktree than the one you are working in. Re-issue this call with \`workspace\` set to the absolute path of your current working directory. (Operators running a raw MCP client that cannot send one can restore the old launch-cwd default with GH_ROUTER_ALLOW_PROXY_CWD_WORKSPACE=1.)`
|
|
35735
|
+
}],
|
|
35736
|
+
isError: true
|
|
35737
|
+
};
|
|
35738
|
+
const effectiveSource = ctx?.workspaceSource ?? (callerGaveWorkspace ? "argument" : "absent");
|
|
35739
|
+
const workspaceNote = effectiveSource === "argument" ? "" : `[workspace: ${workspace} (${effectiveSource === "session" ? "from your session's working directory; pass `workspace` explicitly if you are running somewhere else, such as a git worktree" : "the proxy's launch directory"})]\n\n`;
|
|
35164
35740
|
let maxWallClockMs;
|
|
35165
35741
|
let clampNote = "";
|
|
35166
35742
|
if (args.maxWallClockMs !== void 0) {
|
|
@@ -35188,7 +35764,7 @@ async function runWorkerToolCall(call) {
|
|
|
35188
35764
|
maxWallClockMs,
|
|
35189
35765
|
signal
|
|
35190
35766
|
});
|
|
35191
|
-
const notePrefix = `${clampNote}${worktreeNote}`;
|
|
35767
|
+
const notePrefix = `${workspaceNote}${clampNote}${worktreeNote}`;
|
|
35192
35768
|
return {
|
|
35193
35769
|
content: [{
|
|
35194
35770
|
type: "text",
|
|
@@ -35421,6 +35997,6 @@ function enumerateInjectedMcpToolNames(groupKeys, opts = {}) {
|
|
|
35421
35997
|
return [...new Set(names)];
|
|
35422
35998
|
}
|
|
35423
35999
|
//#endregion
|
|
35424
|
-
export {
|
|
36000
|
+
export { handleMcpDelete as $, resolveLeadSlugArg as $t, satisfiesMinVersion as A, hasSupportedBrowserInstalled as At, rememberThinkingHistoryRepair as B, collapsePathKeys as Bt, availableToolCommands as C, resolveMcpToolTimeoutMs as Ct, vscodeRipgrepPath as D, readResponseBodyCapped as Dt, toolbeltSkipSet as E, MAX_RESPONSE_BODY_BYTES as Et, injectAdvisorTool as F, warmTreeSitterPool as Ft, isControllerClosedError as G, DEFAULT_CODEX_MODEL as Gt, repairRejectedThinkingHistory as H, BUDGET_SMALL_FAST_CATALOG_ID as Ht, isAdvisorRequested as I, provisionTreeSitterAssets as It, relayAnthropicStream as J, UPSTREAM_FETCH_TIMEOUT_MS as Jt, logStreamError as K, DEFAULT_CODEX_MODEL_FALLBACKS as Kt, resolveAdvisorEffort as L, CONDENSED_OPERATING_SEQUENCE as Lt, ADVISOR_INTERNAL_TOOL_NAME as M, provisionAndIndexColbert as Mt, ADVISOR_TOOL_INSTRUCTIONS as N, extractTarGzMember as Nt, TOOLBELT_TOOLS$1 as O, parseJsonOrDiagnose as Ot, buildAdvisorStream as P, extractZipMember as Pt, clampEffort as Q, pickClaudeDefault as Qt, resolveAdvisorModel as R, DEFINITION_OF_GREATNESS as Rt, buildEnv as S, warnOnTokenPriceDrift as St, toolbeltEnabled as T, createChatCompletions as Tt, buildAnthropicErrorEvent as U, BUDGET_SMALL_FAST_SLUG as Ut, repairKnownThinkingHistory as V, toolbeltPathOverride as Vt, buildOpenAIErrorEvent as W, DEFAULT_CLAUDE_MODEL_FALLBACKS as Wt, UNKNOWN_EFFORT_ANCHOR as X, generateRandomPort as Xt, EFFORT_ORDER as Y, UPSTREAM_INACTIVITY_TIMEOUT_MS as Yt, bucketEffort as Z, isBudgetClaudeLead as Zt, appendPlanReminder as _, shimDefaultsToXhigh as _t, buildPeerAwarenessSnippet as a, withInstallLock as an, browserCompoundToolsEnabled as at, resolveWorkerRunOpts as b, getTokenCount as bt, personasFor as c, geminiAvailable as ct, EXPLORE_DEFAULT_MODEL as d, nativeSubagentModel as dt, upstreamAllowH2 as en, handleMcpPost as et, EXPLORE_DEFAULT_THINKING as f, reviewerModel as ft, TEST_DEFAULT_MODEL as g, workerToolsEnabled as gt, REVIEW_DEFAULT_MODEL as h, standInToolEnabled as ht, buildAgentPrompt as i, withOneMSuffixForLead as in, browseAgentEnabled as it, searchWeb as j, colbertDegradedWarning as jt, assetFor as k, provisionBrowserAssets as kt, BROWSE_DEFAULT_MODEL as l, generalPurposeFastModel as lt, PLAN_DEFAULT_MODEL as m, scribeModel as mt, MCP_GROUPS as n, classifyMessagesRoute as nn, artifactToolsEnabled as nt, buildPeerAwarenessSummary as o, browserToolsEnabled as ot, IMPLEMENT_DEFAULT_MODEL as p, scoutModel as pt, readIteratorWithTimeout as q, DEFAULT_PORT as qt, assertMcpToolSurfaceConsistent as r, withOneMSuffix as rn, brainstormModel as rt, enumerateInjectedMcpToolNames as s, fleetToolsEnabled as st, GROUP_META as t, upstreamMaxConnections as tn, agentToolsEnabled as tt, DEFAULT_MODEL_CHAIN as u, implementerFastModel as ut, resolveDefaultModel as v, countTokens as vt, buildToolbeltAwareness as w, createResponses as wt, runWorkerAgent as x, assembleResponsesPayload as xt, resolveModeDefaults as y, createMessages as yt, formatThinkingRepairDecline as z, shouldUseInsecureTls as zt };
|
|
35425
36001
|
|
|
35426
|
-
//# sourceMappingURL=peer-mcp-personas-
|
|
36002
|
+
//# sourceMappingURL=peer-mcp-personas-V6stFvpq.js.map
|