ghc-proxy 0.8.1 → 0.9.1
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 +4 -4
- package/dist/main.mjs +796 -378
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -5739,7 +5739,8 @@ const reasoningEffortSchema = _enum([
|
|
|
5739
5739
|
"low",
|
|
5740
5740
|
"medium",
|
|
5741
5741
|
"high",
|
|
5742
|
-
"xhigh"
|
|
5742
|
+
"xhigh",
|
|
5743
|
+
"max"
|
|
5743
5744
|
]);
|
|
5744
5745
|
const configFileSchema = object({
|
|
5745
5746
|
githubToken: string().optional(),
|
|
@@ -5759,6 +5760,7 @@ const configFileSchema = object({
|
|
|
5759
5760
|
params: array(string()).min(1)
|
|
5760
5761
|
})).optional(),
|
|
5761
5762
|
responsesApiParameterFiltersReplaceDefault: boolean().optional(),
|
|
5763
|
+
chatCompletionsUseMaxCompletionTokens: array(string()).optional(),
|
|
5762
5764
|
responsesOfficialEmulator: boolean().optional(),
|
|
5763
5765
|
responsesOfficialEmulatorTtlSeconds: number().int().positive().optional(),
|
|
5764
5766
|
modelReasoningEfforts: record(string(), reasoningEffortSchema).optional(),
|
|
@@ -5885,6 +5887,14 @@ var ConfigStore = class {
|
|
|
5885
5887
|
getModelRewrites() {
|
|
5886
5888
|
return getCachedConfig().modelRewrites ?? [];
|
|
5887
5889
|
}
|
|
5890
|
+
/**
|
|
5891
|
+
* Extra model globs whose `/chat/completions` requests must send
|
|
5892
|
+
* `max_completion_tokens` instead of `max_tokens`. Adds to the built-in
|
|
5893
|
+
* evidence-backed list rather than replacing it.
|
|
5894
|
+
*/
|
|
5895
|
+
getChatCompletionsMaxCompletionTokensModels() {
|
|
5896
|
+
return getCachedConfig().chatCompletionsUseMaxCompletionTokens ?? [];
|
|
5897
|
+
}
|
|
5888
5898
|
getModelFallback() {
|
|
5889
5899
|
return getCachedConfig().modelFallback;
|
|
5890
5900
|
}
|
|
@@ -5897,14 +5907,30 @@ const RESPONSES_ENDPOINT = "/responses";
|
|
|
5897
5907
|
* Models whose upstream `/v1/messages` endpoint rejects the `output_config`
|
|
5898
5908
|
* field with "Extra inputs are not permitted".
|
|
5899
5909
|
*
|
|
5900
|
-
* Verified via `scripts/probes/messages/output-config.ts` (2026-03-14)
|
|
5910
|
+
* Verified via `scripts/probes/messages/output-config.ts` (2026-03-14); the
|
|
5911
|
+
* probe enumerates the live `/models` surface, so this list only ever covers
|
|
5912
|
+
* models that existed on the probe date. `claude-sonnet-4` was dropped
|
|
5913
|
+
* 2026-07-26 after leaving that surface.
|
|
5901
5914
|
* When new models appear, re-run the probe and update this list.
|
|
5902
5915
|
*/
|
|
5903
|
-
const MODELS_REJECTING_OUTPUT_CONFIG = new Set([
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5916
|
+
const MODELS_REJECTING_OUTPUT_CONFIG = new Set(["claude-sonnet-4.5", "claude-haiku-4.5"]);
|
|
5917
|
+
/**
|
|
5918
|
+
* Models that advertise `structured_outputs` but cannot serve it on native
|
|
5919
|
+
* `/v1/messages`, because a Google Cloud organization policy blocks the feature
|
|
5920
|
+
* for the Vertex-served deployment:
|
|
5921
|
+
*
|
|
5922
|
+
* Organization Policy constraint constraints/vertexai.allowedPartnerModelFeatures
|
|
5923
|
+
* violated for `projects/<id>` attempting to use a disallowed feature
|
|
5924
|
+
* structured_outputs for Partner model claude-opus-4-7
|
|
5925
|
+
*
|
|
5926
|
+
* Probed 2026-07-26 (`scripts/probes/messages/output-format.ts`). This is an
|
|
5927
|
+
* account-scoped policy rather than a protocol limit — it names a specific GCP
|
|
5928
|
+
* project, so another Copilot subscriber may see different results, and the
|
|
5929
|
+
* policy can be changed without notice. It is therefore an explicit ID list
|
|
5930
|
+
* bounded in time, not a rule derived from the payload shape: re-run the probe
|
|
5931
|
+
* when models change, and delete entries that start passing.
|
|
5932
|
+
*/
|
|
5933
|
+
const MODELS_BLOCKING_NATIVE_STRUCTURED_OUTPUT = new Set(["claude-opus-4.7", "claude-sonnet-4.6"]);
|
|
5908
5934
|
var ModelCache = class {
|
|
5909
5935
|
models;
|
|
5910
5936
|
vsCodeVersion;
|
|
@@ -5951,6 +5977,10 @@ var ModelCache = class {
|
|
|
5951
5977
|
if (!model) return true;
|
|
5952
5978
|
return !MODELS_REJECTING_OUTPUT_CONFIG.has(model.id);
|
|
5953
5979
|
}
|
|
5980
|
+
supportsStructuredOutputs(model) {
|
|
5981
|
+
if (!model || MODELS_BLOCKING_NATIVE_STRUCTURED_OUTPUT.has(model.id)) return false;
|
|
5982
|
+
return model.capabilities.supports.structured_outputs ?? false;
|
|
5983
|
+
}
|
|
5954
5984
|
};
|
|
5955
5985
|
const modelCache = new ModelCache();
|
|
5956
5986
|
//#endregion
|
|
@@ -6005,7 +6035,21 @@ function isTransientUpstreamStatus(status) {
|
|
|
6005
6035
|
function isCapacityLimitStatus(status) {
|
|
6006
6036
|
return status === 429 || status === 529;
|
|
6007
6037
|
}
|
|
6038
|
+
/**
|
|
6039
|
+
* Reject a request locally with a 400.
|
|
6040
|
+
*
|
|
6041
|
+
* Logs on the way out. `onError` in `src/server.ts` returns early for
|
|
6042
|
+
* `code === 'HTTP'` (HTTPError carries its own `toResponse`), so nothing
|
|
6043
|
+
* downstream reports these — without this line a locally rejected request is
|
|
6044
|
+
* indistinguishable in the logs from one that reached upstream and came back
|
|
6045
|
+
* fine, leaving only the access-log status to go on.
|
|
6046
|
+
*/
|
|
6008
6047
|
function throwInvalidRequestError(message, param, code) {
|
|
6048
|
+
consola.warn("Rejected request", {
|
|
6049
|
+
param,
|
|
6050
|
+
code,
|
|
6051
|
+
message
|
|
6052
|
+
});
|
|
6009
6053
|
throw new HTTPError(400, { error: {
|
|
6010
6054
|
message,
|
|
6011
6055
|
type: "invalid_request_error",
|
|
@@ -6014,9 +6058,15 @@ function throwInvalidRequestError(message, param, code) {
|
|
|
6014
6058
|
} });
|
|
6015
6059
|
}
|
|
6016
6060
|
function fromTranslationFailure(failure) {
|
|
6061
|
+
consola.warn("Translation failed", {
|
|
6062
|
+
kind: failure.kind,
|
|
6063
|
+
status: failure.status,
|
|
6064
|
+
message: failure.message
|
|
6065
|
+
});
|
|
6017
6066
|
return new HTTPError(failure.status, { error: {
|
|
6018
6067
|
message: failure.message,
|
|
6019
|
-
type: "translation_error"
|
|
6068
|
+
type: "translation_error",
|
|
6069
|
+
...failure.kind ? { code: failure.kind } : {}
|
|
6020
6070
|
} });
|
|
6021
6071
|
}
|
|
6022
6072
|
function resolveModelOrThrow(modelId) {
|
|
@@ -6152,7 +6202,6 @@ function createResponsesEmulatorState(opts) {
|
|
|
6152
6202
|
const conversationHeadRecords = /* @__PURE__ */ new Map();
|
|
6153
6203
|
const inputItemRecords = /* @__PURE__ */ new Map();
|
|
6154
6204
|
const responseDeletionFlags = /* @__PURE__ */ new Map();
|
|
6155
|
-
const conversationDeletionFlags = /* @__PURE__ */ new Map();
|
|
6156
6205
|
const inputItemDeletionFlags = /* @__PURE__ */ new Map();
|
|
6157
6206
|
const allMaps = [
|
|
6158
6207
|
responseRecords,
|
|
@@ -6160,7 +6209,6 @@ function createResponsesEmulatorState(opts) {
|
|
|
6160
6209
|
conversationHeadRecords,
|
|
6161
6210
|
inputItemRecords,
|
|
6162
6211
|
responseDeletionFlags,
|
|
6163
|
-
conversationDeletionFlags,
|
|
6164
6212
|
inputItemDeletionFlags
|
|
6165
6213
|
];
|
|
6166
6214
|
let pruneIntervalId;
|
|
@@ -6228,7 +6276,6 @@ function createResponsesEmulatorState(opts) {
|
|
|
6228
6276
|
function deletionMap(kind) {
|
|
6229
6277
|
switch (kind) {
|
|
6230
6278
|
case "response": return responseDeletionFlags;
|
|
6231
|
-
case "conversation": return conversationDeletionFlags;
|
|
6232
6279
|
case "input_items": return inputItemDeletionFlags;
|
|
6233
6280
|
}
|
|
6234
6281
|
}
|
|
@@ -6238,7 +6285,6 @@ function createResponsesEmulatorState(opts) {
|
|
|
6238
6285
|
pruneMap(conversationHeadRecords, at);
|
|
6239
6286
|
pruneMap(inputItemRecords, at);
|
|
6240
6287
|
pruneMap(responseDeletionFlags, at);
|
|
6241
|
-
pruneMap(conversationDeletionFlags, at);
|
|
6242
6288
|
pruneMap(inputItemDeletionFlags, at);
|
|
6243
6289
|
}
|
|
6244
6290
|
function evictOldestFromLargestMap() {
|
|
@@ -6267,19 +6313,12 @@ function createResponsesEmulatorState(opts) {
|
|
|
6267
6313
|
}
|
|
6268
6314
|
}
|
|
6269
6315
|
return {
|
|
6270
|
-
isEnabled() {
|
|
6271
|
-
return configStore.isEmulatorEnabled();
|
|
6272
|
-
},
|
|
6273
|
-
getDefaultTtlSeconds() {
|
|
6274
|
-
return configStore.getEmulatorTtlSeconds();
|
|
6275
|
-
},
|
|
6276
6316
|
clear() {
|
|
6277
6317
|
responseRecords.clear();
|
|
6278
6318
|
conversationRecords.clear();
|
|
6279
6319
|
conversationHeadRecords.clear();
|
|
6280
6320
|
inputItemRecords.clear();
|
|
6281
6321
|
responseDeletionFlags.clear();
|
|
6282
|
-
conversationDeletionFlags.clear();
|
|
6283
6322
|
inputItemDeletionFlags.clear();
|
|
6284
6323
|
stopBackgroundPrune();
|
|
6285
6324
|
},
|
|
@@ -6294,7 +6333,7 @@ function createResponsesEmulatorState(opts) {
|
|
|
6294
6333
|
conversations: conversationRecords.size,
|
|
6295
6334
|
conversationHeads: conversationHeadRecords.size,
|
|
6296
6335
|
inputItems: inputItemRecords.size,
|
|
6297
|
-
deletions: responseDeletionFlags.size +
|
|
6336
|
+
deletions: responseDeletionFlags.size + inputItemDeletionFlags.size
|
|
6298
6337
|
};
|
|
6299
6338
|
},
|
|
6300
6339
|
setResponse(response, options) {
|
|
@@ -6303,7 +6342,6 @@ function createResponsesEmulatorState(opts) {
|
|
|
6303
6342
|
const conversationId = responseKeyFromConversation(response.conversation);
|
|
6304
6343
|
writeMap(conversationRecords, conversationId, response.conversation, options?.ttlSeconds);
|
|
6305
6344
|
writeMap(conversationHeadRecords, conversationId, response.id, options?.ttlSeconds);
|
|
6306
|
-
removeDeletionFlag(conversationDeletionFlags, conversationId);
|
|
6307
6345
|
}
|
|
6308
6346
|
return writeMap(responseRecords, response.id, response, options?.ttlSeconds);
|
|
6309
6347
|
},
|
|
@@ -6329,25 +6367,11 @@ function createResponsesEmulatorState(opts) {
|
|
|
6329
6367
|
};
|
|
6330
6368
|
},
|
|
6331
6369
|
setConversation(conversation, options) {
|
|
6332
|
-
|
|
6333
|
-
removeDeletionFlag(conversationDeletionFlags, conversationId);
|
|
6334
|
-
return writeMap(conversationRecords, conversationId, conversation, options?.ttlSeconds);
|
|
6370
|
+
return writeMap(conversationRecords, responseKeyFromConversation(conversation), conversation, options?.ttlSeconds);
|
|
6335
6371
|
},
|
|
6336
6372
|
getConversation(conversationId) {
|
|
6337
|
-
if (readDeletionFlag(conversationDeletionFlags, conversationId)) return;
|
|
6338
6373
|
return readMap(conversationRecords, conversationId);
|
|
6339
6374
|
},
|
|
6340
|
-
deleteConversation(conversationId, options) {
|
|
6341
|
-
pruneExpiredRecords();
|
|
6342
|
-
deleteMapEntry(conversationRecords, conversationId);
|
|
6343
|
-
deleteMapEntry(conversationHeadRecords, conversationId);
|
|
6344
|
-
putDeletionFlag(conversationDeletionFlags, conversationId, options?.ttlSeconds);
|
|
6345
|
-
return {
|
|
6346
|
-
id: conversationId,
|
|
6347
|
-
object: "conversation.deleted",
|
|
6348
|
-
deleted: true
|
|
6349
|
-
};
|
|
6350
|
-
},
|
|
6351
6375
|
setConversationHead(conversationId, responseId, options) {
|
|
6352
6376
|
return writeMap(conversationHeadRecords, conversationId, responseId, options?.ttlSeconds);
|
|
6353
6377
|
},
|
|
@@ -6362,16 +6386,6 @@ function createResponsesEmulatorState(opts) {
|
|
|
6362
6386
|
if (readDeletionFlag(inputItemDeletionFlags, responseId)) return;
|
|
6363
6387
|
return readMap(inputItemRecords, responseId);
|
|
6364
6388
|
},
|
|
6365
|
-
deleteInputItems(responseId, options) {
|
|
6366
|
-
pruneExpiredRecords();
|
|
6367
|
-
deleteMapEntry(inputItemRecords, responseId);
|
|
6368
|
-
putDeletionFlag(inputItemDeletionFlags, responseId, options?.ttlSeconds);
|
|
6369
|
-
return {
|
|
6370
|
-
id: responseId,
|
|
6371
|
-
object: "response.input_items.deleted",
|
|
6372
|
-
deleted: true
|
|
6373
|
-
};
|
|
6374
|
-
},
|
|
6375
6389
|
getDeletionFlag(kind, id) {
|
|
6376
6390
|
return readDeletionFlag(deletionMap(kind), id);
|
|
6377
6391
|
}
|
|
@@ -6619,7 +6633,7 @@ var CopilotClient = class {
|
|
|
6619
6633
|
const { response, release } = await this.request(path, errorMessage, {
|
|
6620
6634
|
method: "POST",
|
|
6621
6635
|
body: JSON.stringify(payload),
|
|
6622
|
-
retryable:
|
|
6636
|
+
retryable: "capacity",
|
|
6623
6637
|
...options
|
|
6624
6638
|
});
|
|
6625
6639
|
if (payload.stream) return withRelease(events(response), release);
|
|
@@ -6653,10 +6667,11 @@ var CopilotClient = class {
|
|
|
6653
6667
|
}
|
|
6654
6668
|
});
|
|
6655
6669
|
}
|
|
6656
|
-
async createEmbeddings(payload) {
|
|
6670
|
+
async createEmbeddings(payload, options) {
|
|
6657
6671
|
return this.requestJson("/embeddings", "Failed to create embeddings", {
|
|
6658
6672
|
method: "POST",
|
|
6659
6673
|
body: JSON.stringify(payload),
|
|
6674
|
+
signal: options?.signal,
|
|
6660
6675
|
retryable: true
|
|
6661
6676
|
});
|
|
6662
6677
|
}
|
|
@@ -6917,6 +6932,27 @@ function normalizeGheDomain(input) {
|
|
|
6917
6932
|
return domain;
|
|
6918
6933
|
}
|
|
6919
6934
|
/**
|
|
6935
|
+
* Resolve the effective GHE domain onto `authStore` and promote the account
|
|
6936
|
+
* type when one is in play.
|
|
6937
|
+
*
|
|
6938
|
+
* Every entry point (`start`, `auth`, `check-usage`) needs both halves: the
|
|
6939
|
+
* domain selects the GitHub URLs, while `accountType` selects the Copilot base
|
|
6940
|
+
* URL in `copilotBaseUrl()`. Applying only the first sends GHE users to the
|
|
6941
|
+
* public Copilot endpoint, so the two are resolved together here rather than
|
|
6942
|
+
* re-derived per command.
|
|
6943
|
+
*
|
|
6944
|
+
* @param auth Mutated in place — receives the resolved domain and, when a
|
|
6945
|
+
* domain is in play, an account type promoted from `individual`.
|
|
6946
|
+
* @param configuredDomain Persisted `gheDomain` from config.json.
|
|
6947
|
+
* @param overrideDomain CLI argument. `undefined` leaves the persisted value
|
|
6948
|
+
* alone; an empty string explicitly clears it.
|
|
6949
|
+
*/
|
|
6950
|
+
function applyGheDomain(auth, configuredDomain, overrideDomain) {
|
|
6951
|
+
auth.gheDomain = configuredDomain;
|
|
6952
|
+
if (overrideDomain !== void 0) auth.gheDomain = overrideDomain ? normalizeGheDomain(overrideDomain) : void 0;
|
|
6953
|
+
if (auth.gheDomain && auth.accountType === "individual") auth.accountType = "enterprise";
|
|
6954
|
+
}
|
|
6955
|
+
/**
|
|
6920
6956
|
* Build GitHub base URL and API base URL for a given GHE domain,
|
|
6921
6957
|
* or return the public GitHub defaults when no domain is provided.
|
|
6922
6958
|
*/
|
|
@@ -6986,7 +7022,7 @@ var UpstreamRequestQueue = class {
|
|
|
6986
7022
|
}
|
|
6987
7023
|
const { status } = response;
|
|
6988
7024
|
const isCapacityLimit = isCapacityLimitStatus(status);
|
|
6989
|
-
if (!(
|
|
7025
|
+
if (!((context.retryable === "capacity" ? isCapacityLimit : context.retryable === true && isTransientUpstreamStatus(status)) && attempt < this.options.maxRetries)) {
|
|
6990
7026
|
if (isCapacityLimit) this.applyCooldown(this.getRetryDelayMs(response, 0));
|
|
6991
7027
|
return {
|
|
6992
7028
|
response,
|
|
@@ -7312,9 +7348,7 @@ async function runAuth(options) {
|
|
|
7312
7348
|
authStore.showToken = options.showToken;
|
|
7313
7349
|
await ensurePaths();
|
|
7314
7350
|
await readConfig();
|
|
7315
|
-
authStore
|
|
7316
|
-
if (options.gheDomain !== void 0) authStore.gheDomain = options.gheDomain ? normalizeGheDomain(options.gheDomain) : void 0;
|
|
7317
|
-
if (authStore.gheDomain && authStore.accountType === "individual") authStore.accountType = "enterprise";
|
|
7351
|
+
applyGheDomain(authStore, getCachedConfig().gheDomain, options.gheDomain);
|
|
7318
7352
|
await cacheVSCodeVersion();
|
|
7319
7353
|
await setupGitHubToken({ force: true });
|
|
7320
7354
|
consola.success("GitHub token written to config.json");
|
|
@@ -7360,7 +7394,7 @@ const checkUsage = defineCommand({
|
|
|
7360
7394
|
async run() {
|
|
7361
7395
|
await ensurePaths();
|
|
7362
7396
|
await readConfig();
|
|
7363
|
-
authStore
|
|
7397
|
+
applyGheDomain(authStore, getCachedConfig().gheDomain);
|
|
7364
7398
|
await cacheVSCodeVersion();
|
|
7365
7399
|
await setupGitHubToken();
|
|
7366
7400
|
try {
|
|
@@ -7385,7 +7419,7 @@ const checkUsage = defineCommand({
|
|
|
7385
7419
|
});
|
|
7386
7420
|
//#endregion
|
|
7387
7421
|
//#region src/util/version.ts
|
|
7388
|
-
const VERSION = "0.
|
|
7422
|
+
const VERSION = "0.9.1";
|
|
7389
7423
|
//#endregion
|
|
7390
7424
|
//#region src/debug.ts
|
|
7391
7425
|
function getRuntimeInfo() {
|
|
@@ -47834,6 +47868,61 @@ function logRequest(method, url, status, elapsed, modelInfo, requestId) {
|
|
|
47834
47868
|
console.log(`${line}${formatModelMapping(modelInfo)}${rid}`);
|
|
47835
47869
|
}
|
|
47836
47870
|
//#endregion
|
|
47871
|
+
//#region src/lib/timeout-error.ts
|
|
47872
|
+
/**
|
|
47873
|
+
* Whether an error represents a request that timed out or was aborted.
|
|
47874
|
+
*
|
|
47875
|
+
* The shape differs by runtime, so the check is structural rather than a
|
|
47876
|
+
* single `name` comparison:
|
|
47877
|
+
* - Bun rejects with a flat `DOMException` named `TimeoutError` (its ~300s
|
|
47878
|
+
* `fetch` ceiling, `AbortSignal.timeout`) or `AbortError`.
|
|
47879
|
+
* - Node rejects with `TypeError('fetch failed' | 'terminated')` and puts the
|
|
47880
|
+
* real undici error on `.cause` (`HeadersTimeoutError`, `BodyTimeoutError`,
|
|
47881
|
+
* `ConnectTimeoutError`), so the top-level error carries no signal at all —
|
|
47882
|
+
* `TypeError('fetch failed')` is also what `ECONNREFUSED` and DNS failures
|
|
47883
|
+
* look like. The discriminator is the cause's `name`/`code`.
|
|
47884
|
+
*
|
|
47885
|
+
* Both runtimes enforce a ~300s upstream ceiling by default (Node's is
|
|
47886
|
+
* undici's `headersTimeout`/`bodyTimeout` default of `300e3`), which fires
|
|
47887
|
+
* long before the configured `--upstream-timeout` of 1800s.
|
|
47888
|
+
*
|
|
47889
|
+
* Kept in one place because the rule is checked on both sides of the stream
|
|
47890
|
+
* boundary: `src/server.ts` maps it to a 504 before the first byte, and the
|
|
47891
|
+
* Anthropic stream transducer maps it to an SSE error frame after. Two
|
|
47892
|
+
* implementations of "what counts as a timeout" is how one of them ends up
|
|
47893
|
+
* recognizing only half the errors.
|
|
47894
|
+
*/
|
|
47895
|
+
const TIMEOUT_ERROR_NAMES = new Set([
|
|
47896
|
+
"AbortError",
|
|
47897
|
+
"TimeoutError",
|
|
47898
|
+
"ConnectTimeoutError",
|
|
47899
|
+
"HeadersTimeoutError",
|
|
47900
|
+
"BodyTimeoutError"
|
|
47901
|
+
]);
|
|
47902
|
+
const TIMEOUT_ERROR_CODES = new Set([
|
|
47903
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
47904
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
47905
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
47906
|
+
"ETIMEDOUT"
|
|
47907
|
+
]);
|
|
47908
|
+
const MAX_CAUSE_DEPTH = 5;
|
|
47909
|
+
function matchesTimeoutShape(value, depth) {
|
|
47910
|
+
if (typeof value !== "object" || value === null) return false;
|
|
47911
|
+
const candidate = value;
|
|
47912
|
+
if (typeof candidate.name === "string" && TIMEOUT_ERROR_NAMES.has(candidate.name)) return true;
|
|
47913
|
+
if (typeof candidate.code === "string" && TIMEOUT_ERROR_CODES.has(candidate.code)) return true;
|
|
47914
|
+
if (depth >= MAX_CAUSE_DEPTH) return false;
|
|
47915
|
+
if (matchesTimeoutShape(candidate.cause, depth + 1)) return true;
|
|
47916
|
+
return Array.isArray(candidate.errors) && candidate.errors.some((inner) => matchesTimeoutShape(inner, depth + 1));
|
|
47917
|
+
}
|
|
47918
|
+
function isTimeoutLikeError(error) {
|
|
47919
|
+
try {
|
|
47920
|
+
return matchesTimeoutShape(error, 0);
|
|
47921
|
+
} catch {
|
|
47922
|
+
return false;
|
|
47923
|
+
}
|
|
47924
|
+
}
|
|
47925
|
+
//#endregion
|
|
47837
47926
|
//#region src/lib/sse-adapter.ts
|
|
47838
47927
|
/**
|
|
47839
47928
|
* Serializes Anthropic stream events into SSE output items
|
|
@@ -47917,6 +48006,20 @@ function inferReasoningEffort(budgetTokens) {
|
|
|
47917
48006
|
if (budgetTokens <= 24e3) return "medium";
|
|
47918
48007
|
return "high";
|
|
47919
48008
|
}
|
|
48009
|
+
/**
|
|
48010
|
+
* The effort to send upstream: the level the caller named, else one inferred
|
|
48011
|
+
* from the thinking budget.
|
|
48012
|
+
*
|
|
48013
|
+
* `output_config.effort` used to be dropped entirely on this path — it is not
|
|
48014
|
+
* part of the OpenAI chat schema, so the normalizer never read it — which
|
|
48015
|
+
* silently downgraded a caller asking for `max` to whatever the budget
|
|
48016
|
+
* heuristic produced (`high` at most). Copilot does accept `reasoning_effort`
|
|
48017
|
+
* here: probed 2026-07-26, every reasoning Claude model on `/chat/completions`
|
|
48018
|
+
* accepts the levels it advertises, `max` included.
|
|
48019
|
+
*/
|
|
48020
|
+
function resolveRequestEffort(request, budgetTokens) {
|
|
48021
|
+
return request.outputEffort ?? inferReasoningEffort(budgetTokens);
|
|
48022
|
+
}
|
|
47920
48023
|
function inferModelFamily(model) {
|
|
47921
48024
|
if (model.startsWith("claude")) return "claude";
|
|
47922
48025
|
if (model.startsWith("gpt") || model.startsWith("o1") || model.startsWith("o3") || model.startsWith("o4")) return "gpt";
|
|
@@ -47930,8 +48033,9 @@ const baseProfile = {
|
|
|
47930
48033
|
includeUsageOnStream: true,
|
|
47931
48034
|
applyThinking(request) {
|
|
47932
48035
|
const thinking = request.thinking;
|
|
47933
|
-
if (
|
|
47934
|
-
|
|
48036
|
+
if (thinking?.type === "disabled") return {};
|
|
48037
|
+
if (!thinking) return request.outputEffort ? { reasoning_effort: request.outputEffort } : {};
|
|
48038
|
+
return { reasoning_effort: resolveRequestEffort(request, thinking.type === "adaptive" ? 24e3 : thinking.budgetTokens) };
|
|
47935
48039
|
}
|
|
47936
48040
|
};
|
|
47937
48041
|
const claudeProfile = {
|
|
@@ -47941,10 +48045,11 @@ const claudeProfile = {
|
|
|
47941
48045
|
includeUsageOnStream: true,
|
|
47942
48046
|
applyThinking(request) {
|
|
47943
48047
|
const thinking = request.thinking;
|
|
47944
|
-
if (
|
|
48048
|
+
if (thinking?.type === "disabled") return {};
|
|
48049
|
+
if (!thinking) return request.outputEffort ? { reasoning_effort: request.outputEffort } : {};
|
|
47945
48050
|
const budgetTokens = thinking.type === "adaptive" ? 24e3 : thinking.budgetTokens;
|
|
47946
48051
|
return {
|
|
47947
|
-
reasoning_effort:
|
|
48052
|
+
reasoning_effort: resolveRequestEffort(request, budgetTokens),
|
|
47948
48053
|
thinking_budget: budgetTokens
|
|
47949
48054
|
};
|
|
47950
48055
|
}
|
|
@@ -48469,6 +48574,7 @@ function buildCapiExecutionPlan(request, options = {}) {
|
|
|
48469
48574
|
stream: request.stream,
|
|
48470
48575
|
temperature: request.temperature,
|
|
48471
48576
|
top_p: request.topP,
|
|
48577
|
+
...request.topK != null ? { top_k: request.topK } : {},
|
|
48472
48578
|
user: request.userId,
|
|
48473
48579
|
tools: serializeTools(request.tools),
|
|
48474
48580
|
tool_choice: serializeToolChoice(request.toolChoice),
|
|
@@ -48727,6 +48833,12 @@ const anthropicThinkingSchema = union([
|
|
|
48727
48833
|
budget_tokens: number().int().positive()
|
|
48728
48834
|
}).loose()
|
|
48729
48835
|
]);
|
|
48836
|
+
/**
|
|
48837
|
+
* `format` stays strict: an unrecognized key here means the caller expects a
|
|
48838
|
+
* constraint the proxy cannot translate, and silently accepting it would let a
|
|
48839
|
+
* schema-constrained request come back unconstrained. Rejecting is the honest
|
|
48840
|
+
* answer — see docs/solutions/integration-issues/claude-code-messages-startup-payloads.md.
|
|
48841
|
+
*/
|
|
48730
48842
|
const anthropicOutputFormatSchema = object({
|
|
48731
48843
|
type: literal("json_schema"),
|
|
48732
48844
|
schema: jsonObjectSchema,
|
|
@@ -48734,16 +48846,24 @@ const anthropicOutputFormatSchema = object({
|
|
|
48734
48846
|
description: string().nullable().optional(),
|
|
48735
48847
|
strict: boolean().optional()
|
|
48736
48848
|
}).strict();
|
|
48849
|
+
/**
|
|
48850
|
+
* `output_config` itself is loose. It is the fastest-moving object in the
|
|
48851
|
+
* Anthropic Messages schema, and it was the only strict container on this
|
|
48852
|
+
* boundary — so every field Anthropic added arrived here as a local 400 before
|
|
48853
|
+
* the request could reach a model that may well accept it. Unknown keys are
|
|
48854
|
+
* forwarded rather than rejected; `format` keeps its own strict contract above,
|
|
48855
|
+
* and `sanitizeOutputConfig` preserves the extras rather than dropping them.
|
|
48856
|
+
*/
|
|
48737
48857
|
const anthropicOutputConfigSchema = object({
|
|
48738
48858
|
effort: _enum([
|
|
48739
48859
|
"low",
|
|
48740
48860
|
"medium",
|
|
48741
48861
|
"high",
|
|
48742
|
-
"
|
|
48743
|
-
"
|
|
48862
|
+
"xhigh",
|
|
48863
|
+
"max"
|
|
48744
48864
|
]).nullable().optional(),
|
|
48745
48865
|
format: anthropicOutputFormatSchema.optional()
|
|
48746
|
-
}).
|
|
48866
|
+
}).loose();
|
|
48747
48867
|
const anthropicMessagesBasePayloadSchema = object({
|
|
48748
48868
|
model: string().min(1),
|
|
48749
48869
|
messages: array(anthropicMessageSchema).min(1),
|
|
@@ -48788,11 +48908,23 @@ function parseEmbeddingRequest(payload) {
|
|
|
48788
48908
|
}
|
|
48789
48909
|
//#endregion
|
|
48790
48910
|
//#region src/types/copilot.ts
|
|
48911
|
+
/**
|
|
48912
|
+
* Effort levels Copilot accepts on `reasoning_effort`.
|
|
48913
|
+
*
|
|
48914
|
+
* Probed 2026-07-26 (`scripts/probes/effort-and-tokens.ts`): a model accepts
|
|
48915
|
+
* exactly the levels it advertises in `capabilities.supports.reasoning_effort`
|
|
48916
|
+
* and rejects the rest with 400, so always clamp against that list rather than
|
|
48917
|
+
* assuming this union is universally valid. `max` is gpt-5.6-and-later on the
|
|
48918
|
+
* Responses boundary, but available across the Claude family.
|
|
48919
|
+
*/
|
|
48791
48920
|
const REASONING_EFFORT_VALUES = [
|
|
48921
|
+
"none",
|
|
48792
48922
|
"minimal",
|
|
48793
48923
|
"low",
|
|
48794
48924
|
"medium",
|
|
48795
|
-
"high"
|
|
48925
|
+
"high",
|
|
48926
|
+
"xhigh",
|
|
48927
|
+
"max"
|
|
48796
48928
|
];
|
|
48797
48929
|
//#endregion
|
|
48798
48930
|
//#region src/ingest/validation/openai-chat.ts
|
|
@@ -48906,9 +49038,19 @@ function parseOpenAIChatPayload(payload) {
|
|
|
48906
49038
|
}
|
|
48907
49039
|
//#endregion
|
|
48908
49040
|
//#region src/ingest/validation/responses.ts
|
|
49041
|
+
/**
|
|
49042
|
+
* Marks where a reusable cache prefix ends. Only models with explicit prompt
|
|
49043
|
+
* caching accept it — probed 2026-07-26, gpt-5.6 returns 200 while gpt-5.5 and
|
|
49044
|
+
* earlier return `400 prompt_cache_breakpoint is not supported on this model`
|
|
49045
|
+
* (`scripts/probes/prompt-caching.ts`).
|
|
49046
|
+
*
|
|
49047
|
+
* `explicit` is the only documented mode.
|
|
49048
|
+
*/
|
|
49049
|
+
const promptCacheBreakpointSchema = object({ mode: literal("explicit") }).loose();
|
|
48909
49050
|
const responsesInputTextSchema = object({
|
|
48910
49051
|
type: _enum(["input_text", "output_text"]),
|
|
48911
|
-
text: string()
|
|
49052
|
+
text: string(),
|
|
49053
|
+
prompt_cache_breakpoint: promptCacheBreakpointSchema.nullable().optional()
|
|
48912
49054
|
}).loose();
|
|
48913
49055
|
const responsesInputImageSchema = object({
|
|
48914
49056
|
type: literal("input_image"),
|
|
@@ -48919,7 +49061,8 @@ const responsesInputImageSchema = object({
|
|
|
48919
49061
|
"high",
|
|
48920
49062
|
"auto",
|
|
48921
49063
|
"original"
|
|
48922
|
-
]).optional()
|
|
49064
|
+
]).optional(),
|
|
49065
|
+
prompt_cache_breakpoint: promptCacheBreakpointSchema.nullable().optional()
|
|
48923
49066
|
}).loose().superRefine((item, ctx) => {
|
|
48924
49067
|
if (!item.image_url && !item.file_id) ctx.addIssue({
|
|
48925
49068
|
code: "custom",
|
|
@@ -49097,14 +49240,7 @@ const responsesToolChoiceSchema = union([
|
|
|
49097
49240
|
}).loose()
|
|
49098
49241
|
]);
|
|
49099
49242
|
const responsesReasoningConfigSchema = object({
|
|
49100
|
-
effort: _enum(
|
|
49101
|
-
"none",
|
|
49102
|
-
"minimal",
|
|
49103
|
-
"low",
|
|
49104
|
-
"medium",
|
|
49105
|
-
"high",
|
|
49106
|
-
"xhigh"
|
|
49107
|
-
]).nullable().optional(),
|
|
49243
|
+
effort: _enum(REASONING_EFFORT_VALUES).nullable().optional(),
|
|
49108
49244
|
generate_summary: _enum([
|
|
49109
49245
|
"auto",
|
|
49110
49246
|
"concise",
|
|
@@ -49162,6 +49298,10 @@ function createResponsesPayloadSchema(options) {
|
|
|
49162
49298
|
stream_options: object({ include_obfuscation: boolean().nullable().optional() }).loose().nullable().optional(),
|
|
49163
49299
|
safety_identifier: string().nullable().optional(),
|
|
49164
49300
|
prompt_cache_key: string().nullable().optional(),
|
|
49301
|
+
prompt_cache_options: object({
|
|
49302
|
+
mode: _enum(["implicit", "explicit"]).optional(),
|
|
49303
|
+
ttl: literal("30m").optional()
|
|
49304
|
+
}).loose().nullable().optional(),
|
|
49165
49305
|
prompt_cache_retention: _enum(["in-memory", "24h"]).nullable().optional(),
|
|
49166
49306
|
truncation: _enum(["auto", "disabled"]).nullable().optional(),
|
|
49167
49307
|
parallel_tool_calls: boolean().nullable().optional(),
|
|
@@ -49333,159 +49473,22 @@ function createUpstreamSignal(clientSignal, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
49333
49473
|
}
|
|
49334
49474
|
/**
|
|
49335
49475
|
* Convenience wrapper that reads the upstream timeout from runtime config.
|
|
49476
|
+
*
|
|
49477
|
+
* This signal is a *total-duration* limit. Both runtimes separately apply an
|
|
49478
|
+
* ~300s **idle** timeout to `fetch` — Bun's is built in, Node's is undici's
|
|
49479
|
+
* `headersTimeout` / `bodyTimeout` default of `300e3` — which resets on every
|
|
49480
|
+
* byte received. A response that keeps streaming therefore runs past 300s and
|
|
49481
|
+
* is bounded only by this signal; a stalled one is rejected at ~300s by the
|
|
49482
|
+
* runtime instead. `isTimeoutLikeError` recognizes both runtimes' shapes so
|
|
49483
|
+
* every path maps to a 504.
|
|
49336
49484
|
*/
|
|
49337
49485
|
function createUpstreamSignalFromConfig(clientSignal) {
|
|
49338
49486
|
return createUpstreamSignal(clientSignal, authStore.upstreamTimeoutSeconds !== void 0 ? authStore.upstreamTimeoutSeconds * 1e3 : void 0);
|
|
49339
49487
|
}
|
|
49340
49488
|
//#endregion
|
|
49341
|
-
//#region src/pipeline/runner.ts
|
|
49342
|
-
async function runPipeline(params, config) {
|
|
49343
|
-
const ingested = protocolRegistry.ingest(config.protocol, params.body, params.headers);
|
|
49344
|
-
const meta = ingested.meta;
|
|
49345
|
-
const payload = config.afterIngest ? config.afterIngest({
|
|
49346
|
-
payload: ingested.payload,
|
|
49347
|
-
meta,
|
|
49348
|
-
headers: params.headers
|
|
49349
|
-
}) : ingested.payload;
|
|
49350
|
-
const transformResult = config.transformChain.apply({
|
|
49351
|
-
model: payload.model,
|
|
49352
|
-
payload,
|
|
49353
|
-
meta: { betaHeaders: meta.betaHeaders }
|
|
49354
|
-
});
|
|
49355
|
-
payload.model = transformResult.model;
|
|
49356
|
-
const selectedModel = transformResult.resolvedModel;
|
|
49357
|
-
const modelMapping = {
|
|
49358
|
-
originalModel: transformResult.trace.length > 0 ? transformResult.trace[0].from : payload.model,
|
|
49359
|
-
steps: transformResult.trace.map((r) => ({
|
|
49360
|
-
tag: r.tag,
|
|
49361
|
-
from: r.from,
|
|
49362
|
-
to: r.to
|
|
49363
|
-
}))
|
|
49364
|
-
};
|
|
49365
|
-
if (config.afterTransform) await config.afterTransform({
|
|
49366
|
-
payload,
|
|
49367
|
-
meta,
|
|
49368
|
-
headers: params.headers,
|
|
49369
|
-
transformResult,
|
|
49370
|
-
selectedModel
|
|
49371
|
-
});
|
|
49372
|
-
const upstreamSignal = createUpstreamSignalFromConfig(params.signal);
|
|
49373
|
-
const copilotClient = createCopilotClient();
|
|
49374
|
-
const ctx = config.buildStrategyContext({
|
|
49375
|
-
payload,
|
|
49376
|
-
meta,
|
|
49377
|
-
headers: params.headers,
|
|
49378
|
-
selectedModel,
|
|
49379
|
-
copilotClient,
|
|
49380
|
-
upstreamSignal,
|
|
49381
|
-
modelMapping
|
|
49382
|
-
});
|
|
49383
|
-
return {
|
|
49384
|
-
result: await config.strategyRegistry.select(selectedModel, ctx).execute(ctx),
|
|
49385
|
-
modelMapping
|
|
49386
|
-
};
|
|
49387
|
-
}
|
|
49388
|
-
//#endregion
|
|
49389
|
-
//#region src/transform/chain.ts
|
|
49390
|
-
function composeModelTransforms(...steps) {
|
|
49391
|
-
return { apply(input) {
|
|
49392
|
-
let current = input.model;
|
|
49393
|
-
const trace = [];
|
|
49394
|
-
let resolvedModel = input.resolvedModel;
|
|
49395
|
-
const payload = input.payload;
|
|
49396
|
-
for (const step of steps) {
|
|
49397
|
-
const output = step.apply({
|
|
49398
|
-
...input,
|
|
49399
|
-
model: current,
|
|
49400
|
-
payload,
|
|
49401
|
-
resolvedModel
|
|
49402
|
-
});
|
|
49403
|
-
if (output === null) continue;
|
|
49404
|
-
const from = current;
|
|
49405
|
-
const to = output.model;
|
|
49406
|
-
if (output.mutatePayload) output.mutatePayload(payload);
|
|
49407
|
-
if (output.tag) trace.push({
|
|
49408
|
-
tag: output.tag,
|
|
49409
|
-
from,
|
|
49410
|
-
to
|
|
49411
|
-
});
|
|
49412
|
-
current = to;
|
|
49413
|
-
if (output.resolvedModel !== void 0) resolvedModel = output.resolvedModel;
|
|
49414
|
-
}
|
|
49415
|
-
if (resolvedModel === void 0) resolvedModel = modelCache.findById(current);
|
|
49416
|
-
return {
|
|
49417
|
-
model: current,
|
|
49418
|
-
resolvedModel,
|
|
49419
|
-
trace
|
|
49420
|
-
};
|
|
49421
|
-
} };
|
|
49422
|
-
}
|
|
49423
|
-
//#endregion
|
|
49424
49489
|
//#region src/transform/constants.ts
|
|
49425
49490
|
const CONTEXT_BETA_RE = /^context-\d+[km]-/;
|
|
49426
49491
|
//#endregion
|
|
49427
|
-
//#region src/transform/request-model-policy.ts
|
|
49428
|
-
const COMPACT_SYSTEM_PROMPT_START = "You are a helpful AI assistant tasked with summarizing conversations";
|
|
49429
|
-
function applyMessagesModelPolicy(payload, options) {
|
|
49430
|
-
const originalModel = payload.model;
|
|
49431
|
-
if (options?.betaUpgraded) return {
|
|
49432
|
-
originalModel,
|
|
49433
|
-
routedModel: originalModel
|
|
49434
|
-
};
|
|
49435
|
-
const smallModel = configStore.getSmallModel();
|
|
49436
|
-
if (!smallModel || !configStore.isCompactSmallModelEnabled() || !isCompactRequest(payload)) return {
|
|
49437
|
-
originalModel,
|
|
49438
|
-
routedModel: originalModel
|
|
49439
|
-
};
|
|
49440
|
-
if (canRouteToSmallModel(payload, modelCache.findById(originalModel), modelCache.findById(smallModel))) {
|
|
49441
|
-
payload.model = smallModel;
|
|
49442
|
-
return {
|
|
49443
|
-
originalModel,
|
|
49444
|
-
routedModel: smallModel,
|
|
49445
|
-
reason: "compact"
|
|
49446
|
-
};
|
|
49447
|
-
}
|
|
49448
|
-
return {
|
|
49449
|
-
originalModel,
|
|
49450
|
-
routedModel: originalModel
|
|
49451
|
-
};
|
|
49452
|
-
}
|
|
49453
|
-
function isCompactRequest(payload) {
|
|
49454
|
-
if (typeof payload.system === "string") return payload.system.startsWith(COMPACT_SYSTEM_PROMPT_START);
|
|
49455
|
-
if (!Array.isArray(payload.system)) return false;
|
|
49456
|
-
return payload.system.some((block) => typeof block.text === "string" && block.text.startsWith(COMPACT_SYSTEM_PROMPT_START));
|
|
49457
|
-
}
|
|
49458
|
-
function canRouteToSmallModel(payload, originalModel, smallModel) {
|
|
49459
|
-
if (!originalModel || !smallModel) return false;
|
|
49460
|
-
const originalEndpoints = new Set(originalModel.supported_endpoints ?? []);
|
|
49461
|
-
const smallEndpoints = new Set(smallModel.supported_endpoints ?? []);
|
|
49462
|
-
for (const endpoint of originalEndpoints) if (!smallEndpoints.has(endpoint)) return false;
|
|
49463
|
-
if (payload.tools?.length && !(smallModel.capabilities.supports.tool_calls ?? false)) return false;
|
|
49464
|
-
if (payload.thinking && !(smallModel.capabilities.supports.adaptive_thinking ?? false)) return false;
|
|
49465
|
-
if (hasVisionInput$1(payload) && !(smallModel.capabilities.supports.vision ?? false)) return false;
|
|
49466
|
-
return true;
|
|
49467
|
-
}
|
|
49468
|
-
function hasVisionInput$1(payload) {
|
|
49469
|
-
return payload.messages.some((message) => containsVisionContent$1(message.content));
|
|
49470
|
-
}
|
|
49471
|
-
function containsVisionContent$1(content) {
|
|
49472
|
-
if (!Array.isArray(content)) return false;
|
|
49473
|
-
return content.some((block) => block.type === "image");
|
|
49474
|
-
}
|
|
49475
|
-
//#endregion
|
|
49476
|
-
//#region src/transform/policy.ts
|
|
49477
|
-
const modelPolicyStep = {
|
|
49478
|
-
tag: "POLICY",
|
|
49479
|
-
apply({ payload, meta }) {
|
|
49480
|
-
const routing = applyMessagesModelPolicy(payload, { betaUpgraded: meta?.betaHeaders?.some((b) => CONTEXT_BETA_RE.test(b)) ?? false });
|
|
49481
|
-
if (!routing.reason) return null;
|
|
49482
|
-
return {
|
|
49483
|
-
model: routing.routedModel,
|
|
49484
|
-
tag: "COMPACT"
|
|
49485
|
-
};
|
|
49486
|
-
}
|
|
49487
|
-
};
|
|
49488
|
-
//#endregion
|
|
49489
49492
|
//#region src/transform/model-rewrite.ts
|
|
49490
49493
|
/**
|
|
49491
49494
|
* Unified model rewrite: user rules → built-in normalization → pass-through.
|
|
@@ -49542,42 +49545,133 @@ function matchesGlob(pattern, value) {
|
|
|
49542
49545
|
return new RegExp(`^${pattern.replace(GLOB_SPECIAL_RE, "\\$&").replace(GLOB_STAR_RE, ".*")}$`).test(value);
|
|
49543
49546
|
}
|
|
49544
49547
|
//#endregion
|
|
49545
|
-
//#region src/transform/
|
|
49546
|
-
const
|
|
49547
|
-
|
|
49548
|
-
|
|
49549
|
-
|
|
49550
|
-
|
|
49551
|
-
|
|
49552
|
-
|
|
49553
|
-
|
|
49554
|
-
|
|
49555
|
-
|
|
49556
|
-
|
|
49557
|
-
|
|
49558
|
-
|
|
49548
|
+
//#region src/transform/request-model-policy.ts
|
|
49549
|
+
const COMPACT_SYSTEM_PROMPT_START = "You are a helpful AI assistant tasked with summarizing conversations";
|
|
49550
|
+
function applyMessagesModelPolicy(payload, options) {
|
|
49551
|
+
const originalModel = payload.model;
|
|
49552
|
+
if (options?.betaUpgraded) return {
|
|
49553
|
+
originalModel,
|
|
49554
|
+
routedModel: originalModel
|
|
49555
|
+
};
|
|
49556
|
+
const smallModel = configStore.getSmallModel();
|
|
49557
|
+
if (!smallModel || !configStore.isCompactSmallModelEnabled() || !isCompactRequest(payload)) return {
|
|
49558
|
+
originalModel,
|
|
49559
|
+
routedModel: originalModel
|
|
49560
|
+
};
|
|
49561
|
+
if (canRouteToSmallModel(payload, modelCache.findById(originalModel), modelCache.findById(smallModel))) {
|
|
49562
|
+
payload.model = smallModel;
|
|
49559
49563
|
return {
|
|
49560
|
-
|
|
49561
|
-
|
|
49562
|
-
|
|
49563
|
-
p.model = result.model;
|
|
49564
|
-
}
|
|
49564
|
+
originalModel,
|
|
49565
|
+
routedModel: smallModel,
|
|
49566
|
+
reason: "compact"
|
|
49565
49567
|
};
|
|
49566
49568
|
}
|
|
49567
|
-
|
|
49569
|
+
return {
|
|
49570
|
+
originalModel,
|
|
49571
|
+
routedModel: originalModel
|
|
49572
|
+
};
|
|
49573
|
+
}
|
|
49574
|
+
function isCompactRequest(payload) {
|
|
49575
|
+
if (typeof payload.system === "string") return payload.system.startsWith(COMPACT_SYSTEM_PROMPT_START);
|
|
49576
|
+
if (!Array.isArray(payload.system)) return false;
|
|
49577
|
+
return payload.system.some((block) => typeof block.text === "string" && block.text.startsWith(COMPACT_SYSTEM_PROMPT_START));
|
|
49578
|
+
}
|
|
49579
|
+
function canRouteToSmallModel(payload, originalModel, smallModel) {
|
|
49580
|
+
if (!originalModel || !smallModel) return false;
|
|
49581
|
+
const originalEndpoints = new Set(originalModel.supported_endpoints ?? []);
|
|
49582
|
+
const smallEndpoints = new Set(smallModel.supported_endpoints ?? []);
|
|
49583
|
+
for (const endpoint of originalEndpoints) if (!smallEndpoints.has(endpoint)) return false;
|
|
49584
|
+
if (payload.tools?.length && !(smallModel.capabilities.supports.tool_calls ?? false)) return false;
|
|
49585
|
+
if (payload.thinking && !(smallModel.capabilities.supports.adaptive_thinking ?? false)) return false;
|
|
49586
|
+
if (hasVisionInput$1(payload) && !(smallModel.capabilities.supports.vision ?? false)) return false;
|
|
49587
|
+
return true;
|
|
49588
|
+
}
|
|
49589
|
+
function hasVisionInput$1(payload) {
|
|
49590
|
+
return payload.messages.some((message) => containsVisionContent$1(message.content));
|
|
49591
|
+
}
|
|
49592
|
+
function containsVisionContent$1(content) {
|
|
49593
|
+
if (!Array.isArray(content)) return false;
|
|
49594
|
+
return content.some((block) => block.type === "image");
|
|
49595
|
+
}
|
|
49568
49596
|
//#endregion
|
|
49569
|
-
//#region src/transform/
|
|
49570
|
-
|
|
49571
|
-
|
|
49572
|
-
|
|
49573
|
-
|
|
49574
|
-
|
|
49575
|
-
|
|
49576
|
-
|
|
49577
|
-
|
|
49578
|
-
|
|
49597
|
+
//#region src/transform/resolve-model.ts
|
|
49598
|
+
/**
|
|
49599
|
+
* Resolve the model a request should be dispatched with.
|
|
49600
|
+
*
|
|
49601
|
+
* Applies, in order:
|
|
49602
|
+
* 1. model rewrite -- user `modelRewrites` rules, then dash/dot normalization
|
|
49603
|
+
* 2. compact small-model routing -- only when `applyPolicy` is set
|
|
49604
|
+
* 3. lookup against the cached Copilot model list
|
|
49605
|
+
*
|
|
49606
|
+
* Order is load-bearing: policy inspects `payload.model`, so the rewrite must
|
|
49607
|
+
* be written back to the payload before policy runs. Both steps mutate
|
|
49608
|
+
* `payload.model` directly, which is also what dispatch reads downstream.
|
|
49609
|
+
*/
|
|
49610
|
+
function resolveRequestModel({ payload, betaHeaders, applyPolicy }) {
|
|
49611
|
+
const originalModel = payload.model;
|
|
49612
|
+
const steps = [];
|
|
49613
|
+
const rewrite = applyModelRewrite(payload);
|
|
49614
|
+
if (rewrite.reason) steps.push({
|
|
49615
|
+
tag: rewrite.reason,
|
|
49616
|
+
from: originalModel,
|
|
49617
|
+
to: rewrite.model
|
|
49618
|
+
});
|
|
49619
|
+
if (applyPolicy) {
|
|
49620
|
+
const routing = applyMessagesModelPolicy(payload, { betaUpgraded: betaHeaders?.some((b) => CONTEXT_BETA_RE.test(b)) ?? false });
|
|
49621
|
+
if (routing.reason) {
|
|
49622
|
+
steps.push({
|
|
49623
|
+
tag: "COMPACT",
|
|
49624
|
+
from: routing.originalModel,
|
|
49625
|
+
to: routing.routedModel
|
|
49626
|
+
});
|
|
49627
|
+
payload.model = routing.routedModel;
|
|
49628
|
+
}
|
|
49579
49629
|
}
|
|
49580
|
-
return
|
|
49630
|
+
return {
|
|
49631
|
+
model: payload.model,
|
|
49632
|
+
resolvedModel: modelCache.findById(payload.model),
|
|
49633
|
+
modelMapping: {
|
|
49634
|
+
originalModel,
|
|
49635
|
+
steps
|
|
49636
|
+
}
|
|
49637
|
+
};
|
|
49638
|
+
}
|
|
49639
|
+
//#endregion
|
|
49640
|
+
//#region src/pipeline/runner.ts
|
|
49641
|
+
async function runPipeline(params, config) {
|
|
49642
|
+
const ingested = protocolRegistry.ingest(config.protocol, params.body, params.headers);
|
|
49643
|
+
const meta = ingested.meta;
|
|
49644
|
+
const payload = config.afterIngest ? config.afterIngest({
|
|
49645
|
+
payload: ingested.payload,
|
|
49646
|
+
meta,
|
|
49647
|
+
headers: params.headers
|
|
49648
|
+
}) : ingested.payload;
|
|
49649
|
+
const { resolvedModel: selectedModel, modelMapping } = resolveRequestModel({
|
|
49650
|
+
payload,
|
|
49651
|
+
betaHeaders: meta.betaHeaders,
|
|
49652
|
+
applyPolicy: config.applyModelPolicy
|
|
49653
|
+
});
|
|
49654
|
+
if (config.afterTransform) await config.afterTransform({
|
|
49655
|
+
payload,
|
|
49656
|
+
meta,
|
|
49657
|
+
headers: params.headers,
|
|
49658
|
+
selectedModel
|
|
49659
|
+
});
|
|
49660
|
+
const upstreamSignal = createUpstreamSignalFromConfig(params.signal);
|
|
49661
|
+
const copilotClient = createCopilotClient();
|
|
49662
|
+
const ctx = config.buildStrategyContext({
|
|
49663
|
+
payload,
|
|
49664
|
+
meta,
|
|
49665
|
+
headers: params.headers,
|
|
49666
|
+
selectedModel,
|
|
49667
|
+
copilotClient,
|
|
49668
|
+
upstreamSignal,
|
|
49669
|
+
modelMapping
|
|
49670
|
+
});
|
|
49671
|
+
return {
|
|
49672
|
+
result: await config.strategyRegistry.select(selectedModel, ctx).execute(ctx),
|
|
49673
|
+
modelMapping
|
|
49674
|
+
};
|
|
49581
49675
|
}
|
|
49582
49676
|
//#endregion
|
|
49583
49677
|
//#region src/translator/responses/signature-codec.ts
|
|
@@ -49649,7 +49743,28 @@ function isOutputConfigEffort(value) {
|
|
|
49649
49743
|
return OUTPUT_CONFIG_EFFORT_RANK.has(value);
|
|
49650
49744
|
}
|
|
49651
49745
|
function normalizeOutputConfigEffort(effort, model) {
|
|
49652
|
-
|
|
49746
|
+
return clampEffortToAdvertised(effort, model?.capabilities.supports.reasoning_effort);
|
|
49747
|
+
}
|
|
49748
|
+
/**
|
|
49749
|
+
* Clamp an effort to the highest level a model actually advertises.
|
|
49750
|
+
*
|
|
49751
|
+
* Probed 2026-07-26 (`scripts/probes/effort-and-tokens.ts`): a model rejects
|
|
49752
|
+
* every level it does not advertise, and the levels are NOT an ordered ladder
|
|
49753
|
+
* every model implements a prefix of — `claude-opus-4.6` and
|
|
49754
|
+
* `claude-sonnet-4.6` advertise `max` but not `xhigh`, and reject `xhigh` while
|
|
49755
|
+
* accepting `max`. So the target must be derived from the advertised list, not
|
|
49756
|
+
* from a fixed fallback.
|
|
49757
|
+
*
|
|
49758
|
+
* Levels outside the Anthropic `output_config` vocabulary (`none`, `minimal`)
|
|
49759
|
+
* are filtered out first: they are valid clamp *inputs* on the Responses
|
|
49760
|
+
* boundary but never valid clamp *targets*, since silently landing on `none`
|
|
49761
|
+
* would disable reasoning the caller asked for.
|
|
49762
|
+
*
|
|
49763
|
+
* Returns undefined when the model advertises nothing usable, which callers
|
|
49764
|
+
* treat as "leave the request alone".
|
|
49765
|
+
*/
|
|
49766
|
+
function clampEffortToAdvertised(effort, advertised) {
|
|
49767
|
+
const supportedEfforts = advertised?.filter(isOutputConfigEffort);
|
|
49653
49768
|
if (!supportedEfforts?.length) return;
|
|
49654
49769
|
if (supportedEfforts.includes(effort)) return effort;
|
|
49655
49770
|
return supportedEfforts.reduce((highest, current) => {
|
|
@@ -49660,6 +49775,45 @@ function normalizeOutputConfigEffort(effort, model) {
|
|
|
49660
49775
|
function hasOutputConfigFormat(payload) {
|
|
49661
49776
|
return payload?.output_config?.format != null;
|
|
49662
49777
|
}
|
|
49778
|
+
/**
|
|
49779
|
+
* Whether {@link reduceOutputFormatForNativeMessages} can produce a
|
|
49780
|
+
* native-acceptable format without dropping a caller guarantee.
|
|
49781
|
+
*
|
|
49782
|
+
* Split from the reducer so strategy selection stays a pure predicate.
|
|
49783
|
+
*/
|
|
49784
|
+
function canReduceOutputFormatForNativeMessages(payload) {
|
|
49785
|
+
const format = payload?.output_config?.format;
|
|
49786
|
+
return !format || format.strict === void 0 && format.description === void 0;
|
|
49787
|
+
}
|
|
49788
|
+
/**
|
|
49789
|
+
* Reduce `output_config.format` to the shape Copilot's native `/v1/messages`
|
|
49790
|
+
* accepts.
|
|
49791
|
+
*
|
|
49792
|
+
* Probed 2026-07-26 (`scripts/probes/messages/output-format.ts`): native
|
|
49793
|
+
* Messages serves a bare `{ type, schema }` on every model that advertises
|
|
49794
|
+
* `structured_outputs`, but rejects every optional Anthropic annotation —
|
|
49795
|
+
* `output_config.format.name: Extra inputs are not permitted`, same for
|
|
49796
|
+
* `description` and `strict`. The Anthropic schema allows all three, so a
|
|
49797
|
+
* caller can legitimately send them.
|
|
49798
|
+
*
|
|
49799
|
+
* Only `name` is dropped here, and only because it is a pure label: Anthropic
|
|
49800
|
+
* documents no effect on the reply, and the Responses translator has to invent
|
|
49801
|
+
* one when the caller omits it. `description` and `strict` both influence the
|
|
49802
|
+
* reply — one guides the model's output, the other promises the schema is
|
|
49803
|
+
* enforced — so {@link canReduceOutputFormatForNativeMessages} keeps those
|
|
49804
|
+
* requests off this path entirely rather than quietly reducing them here.
|
|
49805
|
+
*/
|
|
49806
|
+
function reduceOutputFormatForNativeMessages(payload) {
|
|
49807
|
+
const format = payload.output_config?.format;
|
|
49808
|
+
if (!format || format.name === void 0) return;
|
|
49809
|
+
payload.output_config = {
|
|
49810
|
+
...payload.output_config,
|
|
49811
|
+
format: {
|
|
49812
|
+
type: format.type,
|
|
49813
|
+
schema: format.schema
|
|
49814
|
+
}
|
|
49815
|
+
};
|
|
49816
|
+
}
|
|
49663
49817
|
function budgetTokensToEffort(budget) {
|
|
49664
49818
|
if (budget >= 24e3) return "high";
|
|
49665
49819
|
if (budget >= 8e3) return "medium";
|
|
@@ -49685,6 +49839,15 @@ function convertEnabledThinkingToAdaptive(payload, model) {
|
|
|
49685
49839
|
effort: budgetTokensToEffort(budget)
|
|
49686
49840
|
};
|
|
49687
49841
|
}
|
|
49842
|
+
/**
|
|
49843
|
+
* Normalize `output_config` for the native `/v1/messages` boundary.
|
|
49844
|
+
*
|
|
49845
|
+
* Rebuilds nothing it does not have to: the effort is clamped in place and
|
|
49846
|
+
* every other key is preserved. `output_config` is loose at ingress precisely
|
|
49847
|
+
* so newer Anthropic fields can reach a model that may accept them — dropping
|
|
49848
|
+
* them here would move the failure from a visible 400 to a silent semantic
|
|
49849
|
+
* change, which is worse.
|
|
49850
|
+
*/
|
|
49688
49851
|
function sanitizeOutputConfig(payload, model) {
|
|
49689
49852
|
if (!payload.output_config) return;
|
|
49690
49853
|
if (!modelCache.supportsOutputConfig(model)) {
|
|
@@ -49693,10 +49856,14 @@ function sanitizeOutputConfig(payload, model) {
|
|
|
49693
49856
|
}
|
|
49694
49857
|
const effort = payload.output_config.effort;
|
|
49695
49858
|
if (effort == null) {
|
|
49859
|
+
if (payload.output_config.format) {
|
|
49860
|
+
delete payload.output_config.effort;
|
|
49861
|
+
return;
|
|
49862
|
+
}
|
|
49696
49863
|
delete payload.output_config;
|
|
49697
49864
|
return;
|
|
49698
49865
|
}
|
|
49699
|
-
payload.output_config =
|
|
49866
|
+
payload.output_config.effort = normalizeOutputConfigEffort(effort, model) ?? effort;
|
|
49700
49867
|
}
|
|
49701
49868
|
function normalizeCacheControlBlock(obj) {
|
|
49702
49869
|
if (obj.cache_control && typeof obj.cache_control === "object") obj.cache_control = { type: obj.cache_control.type };
|
|
@@ -49709,11 +49876,223 @@ function sanitizeCacheControl(payload) {
|
|
|
49709
49876
|
}
|
|
49710
49877
|
if (payload.tools) for (const tool of payload.tools) normalizeCacheControlBlock(tool);
|
|
49711
49878
|
}
|
|
49879
|
+
/**
|
|
49880
|
+
* Drop `top_p` when `temperature` is also present.
|
|
49881
|
+
*
|
|
49882
|
+
* Copilot's native `/v1/messages` endpoint rejects the pair outright for
|
|
49883
|
+
* non-reasoning Claude models:
|
|
49884
|
+
*
|
|
49885
|
+
* `temperature` and `top_p` cannot both be specified for this model.
|
|
49886
|
+
* Please use only one.
|
|
49887
|
+
*
|
|
49888
|
+
* Probed 2026-07-26 (`scripts/probes/sampling-params.ts`): reproduced on
|
|
49889
|
+
* claude-sonnet-4.5 and claude-haiku-4.5; every reasoning model accepted both.
|
|
49890
|
+
* Rather than leak a 400 for a combination clients send routinely, the proxy
|
|
49891
|
+
* keeps `temperature` — the more widely used control, and the one both the
|
|
49892
|
+
* Anthropic and OpenAI defaults are expressed in — and drops `top_p`.
|
|
49893
|
+
*
|
|
49894
|
+
* Applied unconditionally on this boundary: models that accept the pair are
|
|
49895
|
+
* unaffected in practice, since sending only `temperature` is always valid.
|
|
49896
|
+
*/
|
|
49897
|
+
function sanitizeExclusiveSamplingParams(payload) {
|
|
49898
|
+
if (payload.temperature === void 0 || payload.top_p === void 0) return;
|
|
49899
|
+
consola.warn(`Dropped top_p=${payload.top_p}: Copilot rejects temperature and top_p together on /v1/messages. Keeping temperature=${payload.temperature}.`);
|
|
49900
|
+
delete payload.top_p;
|
|
49901
|
+
}
|
|
49712
49902
|
//#endregion
|
|
49713
|
-
//#region src/transform/
|
|
49714
|
-
|
|
49715
|
-
|
|
49716
|
-
|
|
49903
|
+
//#region src/transform/parameter-filter.ts
|
|
49904
|
+
/**
|
|
49905
|
+
* Parameters the default rule strips for reasoning models on the Responses
|
|
49906
|
+
* boundary. Reasoning models (gpt-5 family, o-series, codex) reject sampling
|
|
49907
|
+
* parameters upstream with a 400 "Unsupported parameter" error, so the proxy
|
|
49908
|
+
* drops them instead of leaking the incompatibility to the client.
|
|
49909
|
+
*/
|
|
49910
|
+
const DEFAULT_REASONING_UNSUPPORTED_PARAMS = ["temperature", "top_p"];
|
|
49911
|
+
/**
|
|
49912
|
+
* Models that accept a parameter the blanket reasoning rule would otherwise
|
|
49913
|
+
* strip. Probed 2026-07-26 (`scripts/probes/sampling-params.ts`): every
|
|
49914
|
+
* `/responses` reasoning model rejected `temperature`, but `gpt-5.3-codex`
|
|
49915
|
+
* accepted `top_p` (200) while its siblings returned
|
|
49916
|
+
* `Unsupported parameter: 'top_p' is not supported with this model`.
|
|
49917
|
+
*
|
|
49918
|
+
* Copilot does not advertise per-parameter support — `capabilities.supports`
|
|
49919
|
+
* is byte-identical across gpt-5.3-codex, gpt-5.4 and gpt-5.4-mini — so this
|
|
49920
|
+
* cannot be derived and has to be an evidence-backed glob list. Re-run the
|
|
49921
|
+
* probe when new models appear.
|
|
49922
|
+
*/
|
|
49923
|
+
const REASONING_PARAM_EXEMPTIONS = [{
|
|
49924
|
+
models: ["*-codex", "*-codex-*"],
|
|
49925
|
+
params: ["top_p"]
|
|
49926
|
+
}];
|
|
49927
|
+
/**
|
|
49928
|
+
* A reasoning model is any model that advertises one or more
|
|
49929
|
+
* `reasoning_effort` levels. This dynamically covers the full reasoning
|
|
49930
|
+
* family (mini, codex, future point releases) without a hardcoded ID list.
|
|
49931
|
+
*/
|
|
49932
|
+
function isReasoningModel(model) {
|
|
49933
|
+
return modelCache.supportsReasoningEffort(model);
|
|
49934
|
+
}
|
|
49935
|
+
/**
|
|
49936
|
+
* Resolve the set of request parameters to strip for a given model on the
|
|
49937
|
+
* Responses boundary.
|
|
49938
|
+
*
|
|
49939
|
+
* Rule composition:
|
|
49940
|
+
* 1. Built-in default: reasoning models strip {@link DEFAULT_REASONING_UNSUPPORTED_PARAMS}.
|
|
49941
|
+
* Disabled entirely when `responsesApiParameterFiltersReplaceDefault` is true.
|
|
49942
|
+
* 2. User rules (`responsesApiParameterFilters`): every rule whose `models`
|
|
49943
|
+
* glob matches the resolved model id contributes its `params`.
|
|
49944
|
+
*
|
|
49945
|
+
* The result is the union of all matching rules, so user rules ADD to the
|
|
49946
|
+
* default. Setting `responsesApiParameterFiltersReplaceDefault: true` disables
|
|
49947
|
+
* the default so user rules fully OVERWRITE it.
|
|
49948
|
+
*/
|
|
49949
|
+
function resolveStrippedResponsesParams(model) {
|
|
49950
|
+
const params = /* @__PURE__ */ new Set();
|
|
49951
|
+
const modelId = model?.id;
|
|
49952
|
+
if (!configStore.shouldReplaceDefaultParameterFilters() && isReasoningModel(model)) {
|
|
49953
|
+
const exempt = modelId ? resolveReasoningExemptions(modelId) : /* @__PURE__ */ new Set();
|
|
49954
|
+
for (const param of DEFAULT_REASONING_UNSUPPORTED_PARAMS) if (!exempt.has(param)) params.add(param);
|
|
49955
|
+
}
|
|
49956
|
+
if (modelId) {
|
|
49957
|
+
for (const rule of configStore.getResponsesParameterFilters()) if (rule.models.some((pattern) => matchesGlob(pattern, modelId))) for (const param of rule.params) params.add(param);
|
|
49958
|
+
}
|
|
49959
|
+
return params;
|
|
49960
|
+
}
|
|
49961
|
+
/**
|
|
49962
|
+
* Params the default reasoning rule must NOT strip for this model.
|
|
49963
|
+
*
|
|
49964
|
+
* Exemptions only narrow the built-in default — a user rule naming the same
|
|
49965
|
+
* param still strips it, so an operator can always be more conservative than
|
|
49966
|
+
* the probe evidence.
|
|
49967
|
+
*/
|
|
49968
|
+
function resolveReasoningExemptions(modelId) {
|
|
49969
|
+
const exempt = /* @__PURE__ */ new Set();
|
|
49970
|
+
for (const rule of REASONING_PARAM_EXEMPTIONS) if (rule.models.some((pattern) => matchesGlob(pattern, modelId))) for (const param of rule.params) exempt.add(param);
|
|
49971
|
+
return exempt;
|
|
49972
|
+
}
|
|
49973
|
+
/**
|
|
49974
|
+
* Strip unsupported parameters from a Responses payload before dispatch.
|
|
49975
|
+
* Keys are deleted entirely (never set to null) because upstream rejects the
|
|
49976
|
+
* mere presence of the key, not just non-null values.
|
|
49977
|
+
*/
|
|
49978
|
+
function applyResponsesParameterFilters(payload, model) {
|
|
49979
|
+
const strip = resolveStrippedResponsesParams(model);
|
|
49980
|
+
if (strip.size === 0) return;
|
|
49981
|
+
const removed = [];
|
|
49982
|
+
for (const key of strip) if (key in payload) {
|
|
49983
|
+
delete payload[key];
|
|
49984
|
+
removed.push(key);
|
|
49985
|
+
}
|
|
49986
|
+
if (removed.length > 0) consola.debug(`Stripped unsupported responses params for model ${model?.id}: ${removed.join(", ")}`);
|
|
49987
|
+
}
|
|
49988
|
+
/**
|
|
49989
|
+
* Models whose `/chat/completions` endpoint rejects `max_tokens` and require
|
|
49990
|
+
* `max_completion_tokens` instead:
|
|
49991
|
+
*
|
|
49992
|
+
* Unsupported parameter: 'max_tokens' is not supported with this model.
|
|
49993
|
+
* Use 'max_completion_tokens' instead.
|
|
49994
|
+
*
|
|
49995
|
+
* Probed 2026-07-26 (`scripts/probes/effort-and-tokens.ts`): reproduced on
|
|
49996
|
+
* gpt-5.4. Every other reachable model accepted both spellings — though for
|
|
49997
|
+
* reasoning models the two differ in meaning, since `max_tokens` counts
|
|
49998
|
+
* thinking tokens against the budget while `max_completion_tokens` does not.
|
|
49999
|
+
*
|
|
50000
|
+
* Copilot advertises nothing that distinguishes these models, so this is an
|
|
50001
|
+
* evidence-backed glob list. Extend it via `chatCompletionsUseMaxCompletionTokens`.
|
|
50002
|
+
*/
|
|
50003
|
+
const DEFAULT_MAX_COMPLETION_TOKENS_MODELS = ["gpt-5.4", "gpt-5.4-*"];
|
|
50004
|
+
/**
|
|
50005
|
+
* Rename `max_tokens` to `max_completion_tokens` for models that reject the
|
|
50006
|
+
* former. No-op when the caller sent neither, or when the model accepts
|
|
50007
|
+
* `max_tokens` — which is still the majority.
|
|
50008
|
+
*/
|
|
50009
|
+
function applyChatCompletionsTokenParam(payload, model) {
|
|
50010
|
+
if (payload.max_tokens == null) return;
|
|
50011
|
+
const modelId = model?.id;
|
|
50012
|
+
if (!modelId || !requiresMaxCompletionTokens(modelId)) return;
|
|
50013
|
+
const record = payload;
|
|
50014
|
+
record.max_completion_tokens = payload.max_tokens;
|
|
50015
|
+
delete record.max_tokens;
|
|
50016
|
+
consola.debug(`Renamed max_tokens to max_completion_tokens for model ${modelId}`);
|
|
50017
|
+
}
|
|
50018
|
+
function requiresMaxCompletionTokens(modelId) {
|
|
50019
|
+
return [...DEFAULT_MAX_COMPLETION_TOKENS_MODELS, ...configStore.getChatCompletionsMaxCompletionTokensModels()].some((pattern) => matchesGlob(pattern, modelId));
|
|
50020
|
+
}
|
|
50021
|
+
/**
|
|
50022
|
+
* Copilot's `/responses` minimum for `max_output_tokens`:
|
|
50023
|
+
*
|
|
50024
|
+
* Invalid 'max_output_tokens': integer below minimum value.
|
|
50025
|
+
* Expected a value >= 16
|
|
50026
|
+
*
|
|
50027
|
+
* Probed 2026-07-26 (`scripts/probes/effort-and-tokens.ts`): identical across
|
|
50028
|
+
* all 9 `/responses` models. The ceiling is NOT enforced — the advertised
|
|
50029
|
+
* `limits.max_output_tokens` + 1 was accepted everywhere — so only the floor
|
|
50030
|
+
* is clamped.
|
|
50031
|
+
*/
|
|
50032
|
+
const RESPONSES_MIN_OUTPUT_TOKENS = 16;
|
|
50033
|
+
/**
|
|
50034
|
+
* Raise a below-minimum `max_output_tokens` to Copilot's floor.
|
|
50035
|
+
*
|
|
50036
|
+
* The client-facing schema still accepts 0..15: those are valid OpenAI input,
|
|
50037
|
+
* and this floor is a Copilot quirk the proxy absorbs rather than leaks back
|
|
50038
|
+
* as a 400.
|
|
50039
|
+
*/
|
|
50040
|
+
function clampResponsesOutputTokens(payload) {
|
|
50041
|
+
const requested = payload.max_output_tokens;
|
|
50042
|
+
if (requested == null || requested >= RESPONSES_MIN_OUTPUT_TOKENS) return;
|
|
50043
|
+
payload.max_output_tokens = RESPONSES_MIN_OUTPUT_TOKENS;
|
|
50044
|
+
consola.debug(`Raised max_output_tokens from ${requested} to the Copilot minimum of ${RESPONSES_MIN_OUTPUT_TOKENS}`);
|
|
50045
|
+
}
|
|
50046
|
+
/**
|
|
50047
|
+
* Clamp `reasoning.effort` on the OpenAI-facing `/responses` route to a level
|
|
50048
|
+
* the resolved model advertises.
|
|
50049
|
+
*
|
|
50050
|
+
* This route forwards the caller's own vocabulary, so leaving effort alone is
|
|
50051
|
+
* defensible in isolation — but it is not a passthrough in practice. The same
|
|
50052
|
+
* `afterTransform` already strips `temperature`/`top_p` for reasoning models and
|
|
50053
|
+
* raises a below-minimum `max_output_tokens`, so effort was the one parameter
|
|
50054
|
+
* where the proxy knew the request would 400 and forwarded it anyway.
|
|
50055
|
+
*
|
|
50056
|
+
* `none` and `minimal` pass through unranked, matching the Anthropic-to-Responses
|
|
50057
|
+
* path: they are Responses-only levels rather than rungs on the effort ladder,
|
|
50058
|
+
* and clamping `none` upward would invert the caller's intent.
|
|
50059
|
+
*/
|
|
50060
|
+
function clampResponsesReasoningEffort(payload, model) {
|
|
50061
|
+
const requested = payload.reasoning?.effort;
|
|
50062
|
+
if (!requested || requested === "none" || requested === "minimal") return;
|
|
50063
|
+
const clamped = clampEffortToAdvertised(requested, model?.capabilities.supports.reasoning_effort);
|
|
50064
|
+
if (!clamped || clamped === requested) return;
|
|
50065
|
+
payload.reasoning = {
|
|
50066
|
+
...payload.reasoning,
|
|
50067
|
+
effort: clamped
|
|
50068
|
+
};
|
|
50069
|
+
consola.warn(`Lowered reasoning.effort from ${requested} to ${clamped}, the highest level ${model?.id} advertises.`);
|
|
50070
|
+
}
|
|
50071
|
+
/**
|
|
50072
|
+
* Lower a `max_tokens` above the model's advertised ceiling to that ceiling.
|
|
50073
|
+
*
|
|
50074
|
+
* Copilot's native `/v1/messages` enforces its ceiling, unlike `/responses`
|
|
50075
|
+
* where the advertised value is advisory:
|
|
50076
|
+
*
|
|
50077
|
+
* max_tokens: 64001 > 64000, which is the maximum allowed number of output
|
|
50078
|
+
* tokens for claude
|
|
50079
|
+
*
|
|
50080
|
+
* Probed 2026-07-26 (`scripts/probes/effort-and-tokens.ts`). The model record
|
|
50081
|
+
* already carries the bound, so leaking a 400 for a value the proxy could have
|
|
50082
|
+
* corrected is a worse outcome than serving a shorter completion.
|
|
50083
|
+
*
|
|
50084
|
+
* Unlike the `/responses` floor this is a real semantic change — the caller
|
|
50085
|
+
* receives less output than they asked for — so it warns rather than logs at
|
|
50086
|
+
* debug level. No advertised ceiling means no clamp: an unknown bound is not a
|
|
50087
|
+
* reason to guess one.
|
|
50088
|
+
*/
|
|
50089
|
+
function clampMessagesOutputTokens(payload, model) {
|
|
50090
|
+
const ceiling = model?.capabilities.limits.max_output_tokens;
|
|
50091
|
+
const requested = payload.max_tokens;
|
|
50092
|
+
if (ceiling == null || requested == null || requested <= ceiling) return;
|
|
50093
|
+
payload.max_tokens = ceiling;
|
|
50094
|
+
consola.warn(`Lowered max_tokens from ${requested} to ${ceiling}, the ceiling ${model?.id} advertises. Copilot rejects a higher value outright on /v1/messages.`);
|
|
50095
|
+
}
|
|
49717
50096
|
//#endregion
|
|
49718
50097
|
//#region src/translator/anthropic/document.ts
|
|
49719
50098
|
/** Trimmed text of a `content`-source document part, or undefined when it carries none. */
|
|
@@ -49911,6 +50290,7 @@ function normalizeAnthropicRequest(payload) {
|
|
|
49911
50290
|
})),
|
|
49912
50291
|
toolChoice: normalizeToolChoice(payload.tool_choice),
|
|
49913
50292
|
thinking: normalizeThinking(payload.thinking),
|
|
50293
|
+
outputEffort: payload.output_config?.effort ?? void 0,
|
|
49914
50294
|
serviceTier: payload.service_tier
|
|
49915
50295
|
};
|
|
49916
50296
|
}
|
|
@@ -50148,14 +50528,9 @@ var AnthropicStreamTranslator = class {
|
|
|
50148
50528
|
this.state.messageStartSent = true;
|
|
50149
50529
|
}
|
|
50150
50530
|
getErrorMessage(error) {
|
|
50151
|
-
if (
|
|
50531
|
+
if (isTimeoutLikeError(error)) return "Upstream streaming request timed out. Please retry.";
|
|
50152
50532
|
return "An unexpected error occurred during streaming.";
|
|
50153
50533
|
}
|
|
50154
|
-
isTimeoutError(error) {
|
|
50155
|
-
if (error instanceof DOMException) return error.name === "TimeoutError";
|
|
50156
|
-
if (error instanceof Error) return error.name === "TimeoutError";
|
|
50157
|
-
return false;
|
|
50158
|
-
}
|
|
50159
50534
|
toConversationDeltas(chunk) {
|
|
50160
50535
|
if (chunk.choices.length === 0) return [];
|
|
50161
50536
|
const choice = chunk.choices.toSorted((left, right) => left.index - right.index)[0];
|
|
@@ -50385,11 +50760,6 @@ function toConversationTurn(turn) {
|
|
|
50385
50760
|
};
|
|
50386
50761
|
}
|
|
50387
50762
|
function recordAnthropicRequestIssues(request, context) {
|
|
50388
|
-
if (request.topK !== void 0) context.record({
|
|
50389
|
-
kind: "unsupported_top_k",
|
|
50390
|
-
severity: "warning",
|
|
50391
|
-
message: "Anthropic top_k is not supported by the upstream Copilot CAPI payload and was dropped."
|
|
50392
|
-
}, { fatalInStrict: true });
|
|
50393
50763
|
if (request.serviceTier !== void 0) context.record({
|
|
50394
50764
|
kind: "unsupported_service_tier",
|
|
50395
50765
|
severity: "warning",
|
|
@@ -50456,6 +50826,7 @@ function normalizeAnthropicConversation(payload, policy) {
|
|
|
50456
50826
|
stream: normalized.stream,
|
|
50457
50827
|
temperature: normalized.temperature,
|
|
50458
50828
|
topP: normalized.topP,
|
|
50829
|
+
topK: normalized.topK,
|
|
50459
50830
|
userId: normalized.userId,
|
|
50460
50831
|
tools: normalized.tools?.map((tool) => ({
|
|
50461
50832
|
name: tool.name,
|
|
@@ -50463,7 +50834,8 @@ function normalizeAnthropicConversation(payload, policy) {
|
|
|
50463
50834
|
inputSchema: tool.inputSchema
|
|
50464
50835
|
})),
|
|
50465
50836
|
toolChoice: normalized.toolChoice,
|
|
50466
|
-
thinking: normalized.thinking
|
|
50837
|
+
thinking: normalized.thinking,
|
|
50838
|
+
outputEffort: normalized.outputEffort ?? void 0
|
|
50467
50839
|
},
|
|
50468
50840
|
issues: context.getIssues()
|
|
50469
50841
|
};
|
|
@@ -50785,7 +51157,6 @@ async function handleCompletionCore({ body, signal, headers }) {
|
|
|
50785
51157
|
headers
|
|
50786
51158
|
}, {
|
|
50787
51159
|
protocol: "openai-chat",
|
|
50788
|
-
transformChain: chatCompletionsModelChain,
|
|
50789
51160
|
strategyRegistry: chatCompletionsStrategyRegistry,
|
|
50790
51161
|
afterIngest({ payload }) {
|
|
50791
51162
|
consola.debug("Request payload:", JSON.stringify(payload).slice(-400));
|
|
@@ -50804,6 +51175,7 @@ async function handleCompletionCore({ body, signal, headers }) {
|
|
|
50804
51175
|
payload.max_tokens = selectedModel?.capabilities.limits.max_output_tokens;
|
|
50805
51176
|
consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
|
|
50806
51177
|
}
|
|
51178
|
+
applyChatCompletionsTokenParam(payload, selectedModel);
|
|
50807
51179
|
},
|
|
50808
51180
|
buildStrategyContext({ payload, meta, copilotClient, upstreamSignal, modelMapping }) {
|
|
50809
51181
|
return {
|
|
@@ -50841,16 +51213,27 @@ function normalizeEmbeddingRequest(payload) {
|
|
|
50841
51213
|
}
|
|
50842
51214
|
/**
|
|
50843
51215
|
* Core handler for creating embeddings.
|
|
51216
|
+
*
|
|
51217
|
+
* `client` is an injection seam for tests; production callers omit it. This
|
|
51218
|
+
* route does not go through `runPipeline`, so it constructs its own client and
|
|
51219
|
+
* derives its own upstream signal — otherwise a client disconnect would leave
|
|
51220
|
+
* the upstream request running and the configured timeout unenforced.
|
|
50844
51221
|
*/
|
|
50845
|
-
async function handleEmbeddingsCore(body, headers) {
|
|
51222
|
+
async function handleEmbeddingsCore(body, headers, client, signal) {
|
|
50846
51223
|
const { payload } = protocolRegistry.ingest("embeddings", body, headers);
|
|
50847
|
-
|
|
51224
|
+
const copilotClient = client ?? createCopilotClient();
|
|
51225
|
+
const upstreamSignal = signal ? createUpstreamSignalFromConfig(signal) : void 0;
|
|
51226
|
+
try {
|
|
51227
|
+
return await copilotClient.createEmbeddings(normalizeEmbeddingRequest(payload), { signal: upstreamSignal?.signal });
|
|
51228
|
+
} finally {
|
|
51229
|
+
upstreamSignal?.cleanup();
|
|
51230
|
+
}
|
|
50848
51231
|
}
|
|
50849
51232
|
//#endregion
|
|
50850
51233
|
//#region src/routes/embeddings/route.ts
|
|
50851
51234
|
function createEmbeddingRoutes() {
|
|
50852
51235
|
return new Elysia().use(requestGuardPlugin).post("/embeddings", async ({ body, request }) => {
|
|
50853
|
-
return handleEmbeddingsCore(body, request.headers);
|
|
51236
|
+
return handleEmbeddingsCore(body, request.headers, void 0, request.signal);
|
|
50854
51237
|
}, { guarded: true });
|
|
50855
51238
|
}
|
|
50856
51239
|
//#endregion
|
|
@@ -50904,6 +51287,20 @@ async function handleCountTokensCore({ body, headers }) {
|
|
|
50904
51287
|
return { input_tokens: finalTokenCount };
|
|
50905
51288
|
}
|
|
50906
51289
|
//#endregion
|
|
51290
|
+
//#region src/transform/beta-headers.ts
|
|
51291
|
+
const COPILOT_UNSUPPORTED_BETA_RE = /^mid-conversation-system-\d{4}-\d{2}-\d{2}$/;
|
|
51292
|
+
function processAnthropicBetaHeader(rawHeader) {
|
|
51293
|
+
if (!rawHeader) return void 0;
|
|
51294
|
+
const values = rawHeader.split(",").map((v) => v.trim()).filter(Boolean);
|
|
51295
|
+
const filtered = [];
|
|
51296
|
+
for (const value of values) {
|
|
51297
|
+
if (CONTEXT_BETA_RE.test(value)) continue;
|
|
51298
|
+
if (COPILOT_UNSUPPORTED_BETA_RE.test(value)) continue;
|
|
51299
|
+
filtered.push(value);
|
|
51300
|
+
}
|
|
51301
|
+
return filtered.length > 0 ? filtered.join(",") : void 0;
|
|
51302
|
+
}
|
|
51303
|
+
//#endregion
|
|
50907
51304
|
//#region src/transform/context-management.ts
|
|
50908
51305
|
/** Default token threshold when model limits are unknown. */
|
|
50909
51306
|
const DEFAULT_COMPACT_THRESHOLD = 5e4;
|
|
@@ -50965,59 +51362,32 @@ function containsVisionContent(value) {
|
|
|
50965
51362
|
return false;
|
|
50966
51363
|
}
|
|
50967
51364
|
//#endregion
|
|
50968
|
-
//#region src/transform/
|
|
51365
|
+
//#region src/transform/responses-input.ts
|
|
50969
51366
|
/**
|
|
50970
|
-
*
|
|
50971
|
-
* boundary. Reasoning models (gpt-5 family, o-series, codex) reject sampling
|
|
50972
|
-
* parameters upstream with a 400 "Unsupported parameter" error, so the proxy
|
|
50973
|
-
* drops them instead of leaking the incompatibility to the client.
|
|
50974
|
-
*/
|
|
50975
|
-
const DEFAULT_REASONING_UNSUPPORTED_PARAMS = ["temperature", "top_p"];
|
|
50976
|
-
/**
|
|
50977
|
-
* A reasoning model is any model that advertises one or more
|
|
50978
|
-
* `reasoning_effort` levels. This dynamically covers the full reasoning
|
|
50979
|
-
* family (mini, codex, future point releases) without a hardcoded ID list.
|
|
50980
|
-
*/
|
|
50981
|
-
function isReasoningModel(model) {
|
|
50982
|
-
return modelCache.supportsReasoningEffort(model);
|
|
50983
|
-
}
|
|
50984
|
-
/**
|
|
50985
|
-
* Resolve the set of request parameters to strip for a given model on the
|
|
50986
|
-
* Responses boundary.
|
|
51367
|
+
* Strip `phase` from input message items.
|
|
50987
51368
|
*
|
|
50988
|
-
*
|
|
50989
|
-
*
|
|
50990
|
-
*
|
|
50991
|
-
*
|
|
50992
|
-
* glob matches the resolved model id contributes its `params`.
|
|
51369
|
+
* `phase` (`commentary` / `final_answer`) is an output-only annotation. Some
|
|
51370
|
+
* models reject it when it is sent back as input — this repo traced an
|
|
51371
|
+
* upstream `400 invalid_request_body` to exactly that (see
|
|
51372
|
+
* `docs/investigation-responses-404.md`).
|
|
50993
51373
|
*
|
|
50994
|
-
*
|
|
50995
|
-
*
|
|
50996
|
-
* the
|
|
50997
|
-
|
|
50998
|
-
function resolveStrippedResponsesParams(model) {
|
|
50999
|
-
const params = /* @__PURE__ */ new Set();
|
|
51000
|
-
if (!configStore.shouldReplaceDefaultParameterFilters() && isReasoningModel(model)) for (const param of DEFAULT_REASONING_UNSUPPORTED_PARAMS) params.add(param);
|
|
51001
|
-
const modelId = model?.id;
|
|
51002
|
-
if (modelId) {
|
|
51003
|
-
for (const rule of configStore.getResponsesParameterFilters()) if (rule.models.some((pattern) => matchesGlob(pattern, modelId))) for (const param of rule.params) params.add(param);
|
|
51004
|
-
}
|
|
51005
|
-
return params;
|
|
51006
|
-
}
|
|
51007
|
-
/**
|
|
51008
|
-
* Strip unsupported parameters from a Responses payload before dispatch.
|
|
51009
|
-
* Keys are deleted entirely (never set to null) because upstream rejects the
|
|
51010
|
-
* mere presence of the key, not just non-null values.
|
|
51374
|
+
* Both Responses dispatch paths must call this. `POST /responses` receives the
|
|
51375
|
+
* field from clients replaying prior output; the `/v1/messages` → Responses
|
|
51376
|
+
* strategy has the translator *generate* it (see `resolveAssistantPhase` in
|
|
51377
|
+
* `translator/responses/response-items.ts`), so neither path is exempt.
|
|
51011
51378
|
*/
|
|
51012
|
-
function
|
|
51013
|
-
|
|
51014
|
-
|
|
51015
|
-
const
|
|
51016
|
-
|
|
51017
|
-
|
|
51018
|
-
|
|
51379
|
+
function stripPhaseFromInputMessages(payload) {
|
|
51380
|
+
if (!Array.isArray(payload.input)) return;
|
|
51381
|
+
let stripped = 0;
|
|
51382
|
+
for (const item of payload.input) {
|
|
51383
|
+
if (typeof item !== "object" || item === null) continue;
|
|
51384
|
+
const rec = item;
|
|
51385
|
+
if ((!("type" in rec) || rec.type === "message") && "phase" in rec) {
|
|
51386
|
+
delete rec.phase;
|
|
51387
|
+
stripped++;
|
|
51388
|
+
}
|
|
51019
51389
|
}
|
|
51020
|
-
if (
|
|
51390
|
+
if (stripped > 0) consola.debug(`Stripped phase from ${stripped} input message item(s)`);
|
|
51021
51391
|
}
|
|
51022
51392
|
//#endregion
|
|
51023
51393
|
//#region src/translator/responses/function-schema.ts
|
|
@@ -51239,6 +51609,7 @@ function translateAnthropicToResponsesPayload(payload, options) {
|
|
|
51239
51609
|
instructions: translateSystemPrompt(payload.system),
|
|
51240
51610
|
temperature: payload.temperature ?? null,
|
|
51241
51611
|
top_p: payload.top_p ?? null,
|
|
51612
|
+
...payload.top_k !== void 0 ? { top_k: payload.top_k } : {},
|
|
51242
51613
|
max_output_tokens: payload.max_tokens,
|
|
51243
51614
|
tools: convertAnthropicTools(payload.tools),
|
|
51244
51615
|
tool_choice: convertAnthropicToolChoice(payload.tool_choice),
|
|
@@ -51408,25 +51779,49 @@ function resolveResponsesTextConfig(payload) {
|
|
|
51408
51779
|
} };
|
|
51409
51780
|
}
|
|
51410
51781
|
}
|
|
51782
|
+
/**
|
|
51783
|
+
* Resolve the Responses `reasoning.effort` for an Anthropic request.
|
|
51784
|
+
*
|
|
51785
|
+
* Every branch below produces a *candidate* the caller did not name directly —
|
|
51786
|
+
* a hardcoded tier, a config default, or a mapped `output_config.effort` — so
|
|
51787
|
+
* all of them are clamped at the single exit. Clamping only the
|
|
51788
|
+
* `output_config.effort` branch left the others able to emit a level the model
|
|
51789
|
+
* rejects: `adaptive` sent `medium` to a model advertising `[high, xhigh, max]`,
|
|
51790
|
+
* and `enabled` sent the configured default with an `as` cast and no check.
|
|
51791
|
+
*/
|
|
51411
51792
|
function resolveResponsesReasoningEffort(payload, options) {
|
|
51793
|
+
const candidate = resolveEffortCandidate(payload, options);
|
|
51794
|
+
if (!candidate) return candidate;
|
|
51795
|
+
return clampResponsesEffort(candidate, options);
|
|
51796
|
+
}
|
|
51797
|
+
function resolveEffortCandidate(payload, options) {
|
|
51412
51798
|
if (payload.thinking?.type === "disabled") return "none";
|
|
51413
|
-
if (payload.output_config?.effort) return
|
|
51799
|
+
if (payload.output_config?.effort) return payload.output_config.effort;
|
|
51414
51800
|
if (payload.thinking?.type === "adaptive") return "medium";
|
|
51415
51801
|
if (payload.thinking?.type === "enabled") return options?.reasoningEffortResolver?.(payload.model) ?? "medium";
|
|
51416
51802
|
}
|
|
51417
|
-
|
|
51418
|
-
|
|
51419
|
-
|
|
51803
|
+
/**
|
|
51804
|
+
* Clamp a Responses effort to what the target model advertises.
|
|
51805
|
+
*
|
|
51806
|
+
* `none` and `minimal` belong to the Responses vocabulary but not to the
|
|
51807
|
+
* Anthropic `output_config` ladder, so they are passed through rather than
|
|
51808
|
+
* ranked: `none` means "do not reason", and clamping it *up* to the model's
|
|
51809
|
+
* highest advertised level would invert the caller's intent. Every model
|
|
51810
|
+
* observed on `/responses` advertises `none` (probed 2026-07-26).
|
|
51811
|
+
*
|
|
51812
|
+
* With no advertised list the effort passes through untouched — with nothing to
|
|
51813
|
+
* derive from, forwarding the request beats guessing at a level that may be both
|
|
51814
|
+
* a downgrade and still unsupported.
|
|
51815
|
+
*/
|
|
51816
|
+
function clampResponsesEffort(effort, options) {
|
|
51817
|
+
if (effort === "none" || effort === "minimal") return effort;
|
|
51818
|
+
return clampEffortToAdvertised(effort, options?.supportedEfforts) ?? effort;
|
|
51420
51819
|
}
|
|
51421
51820
|
function assertResponsesCompatibleRequest(payload) {
|
|
51422
51821
|
if (payload.stop_sequences?.length) throw new TranslationFailure("Anthropic stop_sequences cannot be forwarded through the Responses execution path.", {
|
|
51423
51822
|
status: 400,
|
|
51424
51823
|
kind: "unsupported_stop_sequences"
|
|
51425
51824
|
});
|
|
51426
|
-
if (payload.top_k !== void 0) throw new TranslationFailure("Anthropic top_k is not supported on the Responses execution path.", {
|
|
51427
|
-
status: 400,
|
|
51428
|
-
kind: "unsupported_top_k"
|
|
51429
|
-
});
|
|
51430
51825
|
if (payload.service_tier !== void 0) throw new TranslationFailure("Anthropic service_tier is not supported on the Responses execution path.", {
|
|
51431
51826
|
status: 400,
|
|
51432
51827
|
kind: "unsupported_service_tier"
|
|
@@ -51708,10 +52103,12 @@ function mapResponsesUsage(response) {
|
|
|
51708
52103
|
const inputTokens = response.usage?.input_tokens ?? 0;
|
|
51709
52104
|
const outputTokens = response.usage?.output_tokens ?? 0;
|
|
51710
52105
|
const cachedTokens = response.usage?.input_tokens_details?.cached_tokens;
|
|
52106
|
+
const writtenTokens = response.usage?.input_tokens_details?.cache_write_tokens;
|
|
51711
52107
|
return {
|
|
51712
52108
|
input_tokens: inputTokens - (cachedTokens ?? 0),
|
|
51713
52109
|
output_tokens: outputTokens,
|
|
51714
|
-
...cachedTokens !== void 0 ? { cache_read_input_tokens: cachedTokens } : {}
|
|
52110
|
+
...cachedTokens !== void 0 ? { cache_read_input_tokens: cachedTokens } : {},
|
|
52111
|
+
...writtenTokens ? { cache_creation_input_tokens: writtenTokens } : {}
|
|
51715
52112
|
};
|
|
51716
52113
|
}
|
|
51717
52114
|
function isRecord$1(value) {
|
|
@@ -52164,12 +52561,15 @@ function createMessagesViaResponsesStrategy(copilotClient, responsesPayload, opt
|
|
|
52164
52561
|
//#region src/routes/messages/strategy-registry.ts
|
|
52165
52562
|
const nativeMessagesEntry = {
|
|
52166
52563
|
name: "native-messages",
|
|
52167
|
-
canHandle: (model, ctx) => modelCache.supportsEndpoint(model, "/v1/messages") && !hasOutputConfigFormat(ctx?.anthropicPayload),
|
|
52564
|
+
canHandle: (model, ctx) => modelCache.supportsEndpoint(model, "/v1/messages") && (!hasOutputConfigFormat(ctx?.anthropicPayload) || modelCache.supportsStructuredOutputs(model) && canReduceOutputFormatForNativeMessages(ctx?.anthropicPayload)),
|
|
52168
52565
|
async execute(ctx) {
|
|
52169
52566
|
convertEnabledThinkingToAdaptive(ctx.anthropicPayload, ctx.selectedModel);
|
|
52170
52567
|
filterThinkingBlocksForNativeMessages(ctx.anthropicPayload);
|
|
52171
52568
|
sanitizeOutputConfig(ctx.anthropicPayload, ctx.selectedModel);
|
|
52569
|
+
reduceOutputFormatForNativeMessages(ctx.anthropicPayload);
|
|
52570
|
+
sanitizeExclusiveSamplingParams(ctx.anthropicPayload);
|
|
52172
52571
|
sanitizeCacheControl(ctx.anthropicPayload);
|
|
52572
|
+
clampMessagesOutputTokens(ctx.anthropicPayload, ctx.selectedModel);
|
|
52173
52573
|
return await runStrategy(createNativeMessagesStrategy(ctx.copilotClient, ctx.anthropicPayload, ctx.anthropicBetaHeader, {
|
|
52174
52574
|
signal: ctx.upstreamSignal.signal,
|
|
52175
52575
|
requestContext: ctx.requestContext
|
|
@@ -52180,10 +52580,15 @@ const responsesApiEntry = {
|
|
|
52180
52580
|
name: "responses-api",
|
|
52181
52581
|
canHandle: (model) => modelCache.supportsEndpoint(model, RESPONSES_ENDPOINT),
|
|
52182
52582
|
async execute(ctx) {
|
|
52183
|
-
const responsesPayload = withTranslationErrors(() => translateAnthropicToResponsesPayload(ctx.anthropicPayload, {
|
|
52583
|
+
const responsesPayload = withTranslationErrors(() => translateAnthropicToResponsesPayload(ctx.anthropicPayload, {
|
|
52584
|
+
reasoningEffortResolver: (model) => configStore.getReasoningEffort(model),
|
|
52585
|
+
supportedEfforts: ctx.selectedModel?.capabilities.supports.reasoning_effort
|
|
52586
|
+
}));
|
|
52184
52587
|
applyContextManagement(responsesPayload, ctx.selectedModel?.capabilities.limits.max_prompt_tokens);
|
|
52185
52588
|
compactInputByLatestCompaction(responsesPayload);
|
|
52589
|
+
stripPhaseFromInputMessages(responsesPayload);
|
|
52186
52590
|
applyResponsesParameterFilters(responsesPayload, ctx.selectedModel);
|
|
52591
|
+
clampResponsesOutputTokens(responsesPayload);
|
|
52187
52592
|
const { vision, initiator } = getResponsesRequestOptions(responsesPayload);
|
|
52188
52593
|
return await runStrategy(createMessagesViaResponsesStrategy(ctx.copilotClient, responsesPayload, {
|
|
52189
52594
|
vision,
|
|
@@ -52220,7 +52625,7 @@ async function handleMessagesCore({ body, signal, headers }) {
|
|
|
52220
52625
|
headers
|
|
52221
52626
|
}, {
|
|
52222
52627
|
protocol: "anthropic-messages",
|
|
52223
|
-
|
|
52628
|
+
applyModelPolicy: true,
|
|
52224
52629
|
strategyRegistry: defaultStrategyRegistry,
|
|
52225
52630
|
afterIngest({ payload, headers: reqHeaders }) {
|
|
52226
52631
|
if (consola.level >= 4) consola.debug("Anthropic request payload:", JSON.stringify(payload));
|
|
@@ -52265,9 +52670,13 @@ function createMessageRoutes() {
|
|
|
52265
52670
|
//#region src/routes/models/handler.ts
|
|
52266
52671
|
/**
|
|
52267
52672
|
* Core handler for listing models.
|
|
52673
|
+
*
|
|
52674
|
+
* `client` is an injection seam for tests; production callers omit it. This
|
|
52675
|
+
* route does not go through `runPipeline`, so it constructs its own client on
|
|
52676
|
+
* a cache miss.
|
|
52268
52677
|
*/
|
|
52269
|
-
async function handleModelsCore() {
|
|
52270
|
-
if (!modelCache.getModels()) await cacheModels(createCopilotClient());
|
|
52678
|
+
async function handleModelsCore(client) {
|
|
52679
|
+
if (!modelCache.getModels()) await cacheModels(client ?? createCopilotClient());
|
|
52271
52680
|
return {
|
|
52272
52681
|
object: "list",
|
|
52273
52682
|
data: modelCache.getModels()?.data.map((model) => ({
|
|
@@ -52285,9 +52694,9 @@ async function handleModelsCore() {
|
|
|
52285
52694
|
//#endregion
|
|
52286
52695
|
//#region src/routes/models/route.ts
|
|
52287
52696
|
function createModelRoutes() {
|
|
52288
|
-
return new Elysia().get("/models", async () => {
|
|
52697
|
+
return new Elysia().use(requestGuardPlugin).get("/models", async () => {
|
|
52289
52698
|
return handleModelsCore();
|
|
52290
|
-
});
|
|
52699
|
+
}, { guarded: true });
|
|
52291
52700
|
}
|
|
52292
52701
|
//#endregion
|
|
52293
52702
|
//#region src/routes/responses/emulator.ts
|
|
@@ -52667,7 +53076,6 @@ async function handleResponsesCore({ body, signal, headers }) {
|
|
|
52667
53076
|
headers
|
|
52668
53077
|
}, {
|
|
52669
53078
|
protocol: "responses",
|
|
52670
|
-
transformChain: responsesModelChain,
|
|
52671
53079
|
strategyRegistry: responsesStrategyRegistry,
|
|
52672
53080
|
afterIngest({ payload }) {
|
|
52673
53081
|
originalPayload = payload;
|
|
@@ -52682,6 +53090,8 @@ async function handleResponsesCore({ body, signal, headers }) {
|
|
|
52682
53090
|
if (!modelCache.supportsEndpoint(selectedModel, "/responses")) throwInvalidRequestError("The selected model does not support the responses endpoint.", "model");
|
|
52683
53091
|
applyContextManagement(payload, selectedModel.capabilities.limits.max_prompt_tokens);
|
|
52684
53092
|
applyResponsesParameterFilters(payload, selectedModel);
|
|
53093
|
+
clampResponsesOutputTokens(payload);
|
|
53094
|
+
clampResponsesReasoningEffort(payload, selectedModel);
|
|
52685
53095
|
},
|
|
52686
53096
|
buildStrategyContext({ payload, meta, copilotClient, upstreamSignal }) {
|
|
52687
53097
|
const { vision, initiator } = getResponsesRequestOptions(payload);
|
|
@@ -52754,23 +53164,6 @@ function applyResponsesInputPolicies(payload) {
|
|
|
52754
53164
|
rejectUnsupportedRemoteImageUrls(payload);
|
|
52755
53165
|
}
|
|
52756
53166
|
/**
|
|
52757
|
-
* Strip `phase` from input message items. The `phase` field is an output
|
|
52758
|
-
* annotation that some models may reject when sent back as input.
|
|
52759
|
-
*/
|
|
52760
|
-
function stripPhaseFromInputMessages(payload) {
|
|
52761
|
-
if (!Array.isArray(payload.input)) return;
|
|
52762
|
-
let stripped = 0;
|
|
52763
|
-
for (const item of payload.input) {
|
|
52764
|
-
if (typeof item !== "object" || item === null) continue;
|
|
52765
|
-
const rec = item;
|
|
52766
|
-
if ((!("type" in rec) || rec.type === "message") && "phase" in rec) {
|
|
52767
|
-
delete rec.phase;
|
|
52768
|
-
stripped++;
|
|
52769
|
-
}
|
|
52770
|
-
}
|
|
52771
|
-
if (stripped > 0) consola.debug(`Stripped phase from ${stripped} input message item(s)`);
|
|
52772
|
-
}
|
|
52773
|
-
/**
|
|
52774
53167
|
* Remove input items that Copilot cannot resolve and would trigger 404:
|
|
52775
53168
|
* - `item_reference` items (opaque IDs from store=true sessions)
|
|
52776
53169
|
* - `function_call_output` items whose `call_id` has no matching prior
|
|
@@ -52843,35 +53236,42 @@ var UpstreamResourceDispatcher = class {
|
|
|
52843
53236
|
return this.client.deleteResponse(responseId, options);
|
|
52844
53237
|
}
|
|
52845
53238
|
};
|
|
52846
|
-
|
|
52847
|
-
|
|
53239
|
+
/**
|
|
53240
|
+
* Build the dispatcher backing the `/responses/{id}` resource routes.
|
|
53241
|
+
*
|
|
53242
|
+
* `client` is an injection seam for tests: pass a stand-in to exercise the
|
|
53243
|
+
* upstream path without patching `CopilotClient.prototype`. Production callers
|
|
53244
|
+
* omit it and get the configured client.
|
|
53245
|
+
*/
|
|
53246
|
+
function createResourceDispatcher(client) {
|
|
53247
|
+
return configStore.isEmulatorEnabled() ? new EmulatorResourceDispatcher() : new UpstreamResourceDispatcher(client ?? createCopilotClient());
|
|
52848
53248
|
}
|
|
52849
53249
|
//#endregion
|
|
52850
53250
|
//#region src/routes/responses/resource-handler.ts
|
|
52851
|
-
async function handleRetrieveResponseCore({ params, url, headers, signal }) {
|
|
53251
|
+
async function handleRetrieveResponseCore({ params, url, headers, signal, client }) {
|
|
52852
53252
|
const responseId = requireResponseId(params.responseId);
|
|
52853
|
-
return await createResourceDispatcher().retrieve(responseId, getRetrieveParamsFromUrl(url), {
|
|
53253
|
+
return await createResourceDispatcher(client).retrieve(responseId, getRetrieveParamsFromUrl(url), {
|
|
52854
53254
|
requestContext: readCapiRequestContext(headers),
|
|
52855
53255
|
signal
|
|
52856
53256
|
});
|
|
52857
53257
|
}
|
|
52858
|
-
async function handleListResponseInputItemsCore({ params, url, headers, signal }) {
|
|
53258
|
+
async function handleListResponseInputItemsCore({ params, url, headers, signal, client }) {
|
|
52859
53259
|
const responseId = requireResponseId(params.responseId);
|
|
52860
|
-
return await createResourceDispatcher().listInputItems(responseId, getInputItemsParamsFromUrl(url), {
|
|
53260
|
+
return await createResourceDispatcher(client).listInputItems(responseId, getInputItemsParamsFromUrl(url), {
|
|
52861
53261
|
requestContext: readCapiRequestContext(headers),
|
|
52862
53262
|
signal
|
|
52863
53263
|
});
|
|
52864
53264
|
}
|
|
52865
|
-
async function handleCreateResponseInputTokensCore({ body, headers, signal }) {
|
|
53265
|
+
async function handleCreateResponseInputTokensCore({ body, headers, signal, client }) {
|
|
52866
53266
|
const { payload, meta } = protocolRegistry.ingest("responses-input-tokens", body, headers);
|
|
52867
|
-
return await createResourceDispatcher().createInputTokens(payload, {
|
|
53267
|
+
return await createResourceDispatcher(client).createInputTokens(payload, {
|
|
52868
53268
|
requestContext: meta.requestContext,
|
|
52869
53269
|
signal
|
|
52870
53270
|
});
|
|
52871
53271
|
}
|
|
52872
|
-
async function handleDeleteResponseCore({ params, headers, signal }) {
|
|
53272
|
+
async function handleDeleteResponseCore({ params, headers, signal, client }) {
|
|
52873
53273
|
const responseId = requireResponseId(params.responseId);
|
|
52874
|
-
return await createResourceDispatcher().delete(responseId, {
|
|
53274
|
+
return await createResourceDispatcher(client).delete(responseId, {
|
|
52875
53275
|
requestContext: readCapiRequestContext(headers),
|
|
52876
53276
|
signal
|
|
52877
53277
|
});
|
|
@@ -53002,6 +53402,33 @@ function createUsageRoute() {
|
|
|
53002
53402
|
//#endregion
|
|
53003
53403
|
//#region src/server.ts
|
|
53004
53404
|
const isBun = typeof globalThis.Bun !== "undefined";
|
|
53405
|
+
/**
|
|
53406
|
+
* Maps a thrown error to a client response.
|
|
53407
|
+
*
|
|
53408
|
+
* `set.status` is written on every branch because `onError` returns a fresh
|
|
53409
|
+
* `Response` instead of falling through Elysia's normal path — `set.status`
|
|
53410
|
+
* would otherwise still hold whatever it was before the throw, and the access
|
|
53411
|
+
* log in `onAfterResponse` reads it. Without the write-back, a 504 is logged
|
|
53412
|
+
* as a 500.
|
|
53413
|
+
*
|
|
53414
|
+
* Exported so tests exercise this mapping rather than a copy of it.
|
|
53415
|
+
*/
|
|
53416
|
+
function handleRouteError({ code, error, set }) {
|
|
53417
|
+
if (code === "HTTP") return;
|
|
53418
|
+
if (isTimeoutLikeError(error)) {
|
|
53419
|
+
set.status = 504;
|
|
53420
|
+
return Response.json({ error: {
|
|
53421
|
+
message: "Upstream request timed out before a response was received.",
|
|
53422
|
+
type: "timeout_error"
|
|
53423
|
+
} }, { status: 504 });
|
|
53424
|
+
}
|
|
53425
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53426
|
+
set.status = 500;
|
|
53427
|
+
return Response.json({ error: {
|
|
53428
|
+
message,
|
|
53429
|
+
type: "error"
|
|
53430
|
+
} }, { status: 500 });
|
|
53431
|
+
}
|
|
53005
53432
|
function createServer(options) {
|
|
53006
53433
|
return new Elysia({
|
|
53007
53434
|
adapter: isBun ? void 0 : node(),
|
|
@@ -53021,18 +53448,11 @@ function createServer(options) {
|
|
|
53021
53448
|
const elapsed = formatElapsed(requestStart);
|
|
53022
53449
|
const status = typeof set.status === "number" ? set.status : 200;
|
|
53023
53450
|
logRequest(request.method, request.url, status, elapsed, getRequestModelMapping(request), requestId);
|
|
53024
|
-
}).onError(({ code, error }) => {
|
|
53025
|
-
|
|
53026
|
-
|
|
53027
|
-
|
|
53028
|
-
|
|
53029
|
-
} }, { status: 504 });
|
|
53030
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
53031
|
-
return Response.json({ error: {
|
|
53032
|
-
message,
|
|
53033
|
-
type: "error"
|
|
53034
|
-
} }, { status: 500 });
|
|
53035
|
-
}).get("/", () => "Server running").get("/health", () => ({
|
|
53451
|
+
}).onError(({ code, error, set }) => handleRouteError({
|
|
53452
|
+
code,
|
|
53453
|
+
error,
|
|
53454
|
+
set
|
|
53455
|
+
})).get("/", () => "Server running").get("/health", () => ({
|
|
53036
53456
|
status: "ok",
|
|
53037
53457
|
copilotToken: !!authStore.copilotToken,
|
|
53038
53458
|
modelsLoaded: !!modelCache.getModels(),
|
|
@@ -53098,9 +53518,7 @@ async function runServer(options) {
|
|
|
53098
53518
|
baseDelayMs: secondsToMs(upstreamQueueBaseDelaySeconds),
|
|
53099
53519
|
maxDelayMs: secondsToMs(upstreamQueueMaxDelaySeconds)
|
|
53100
53520
|
});
|
|
53101
|
-
authStore.gheDomain
|
|
53102
|
-
if (options.gheDomain !== void 0) authStore.gheDomain = options.gheDomain ? normalizeGheDomain(options.gheDomain) : void 0;
|
|
53103
|
-
if (authStore.gheDomain && authStore.accountType === "individual") authStore.accountType = "enterprise";
|
|
53521
|
+
applyGheDomain(authStore, cachedConfig.gheDomain, options.gheDomain);
|
|
53104
53522
|
await cacheVSCodeVersion();
|
|
53105
53523
|
if (!options.githubToken) await setupGitHubToken();
|
|
53106
53524
|
const tokenCleanup = await setupCopilotToken();
|
|
@@ -53207,7 +53625,7 @@ const start = defineCommand({
|
|
|
53207
53625
|
"upstream-timeout": {
|
|
53208
53626
|
type: "string",
|
|
53209
53627
|
default: "1800",
|
|
53210
|
-
description: "Upstream request timeout in seconds (0 to disable)"
|
|
53628
|
+
description: "Upstream request timeout in seconds (0 to disable). Enforced as a total-duration limit; both runtimes additionally apply their own ~300s idle timeout to fetch, which a steadily streaming response does not trip."
|
|
53211
53629
|
},
|
|
53212
53630
|
"upstream-queue-concurrency": {
|
|
53213
53631
|
type: "string",
|