ghc-proxy 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/main.mjs +736 -375
- package/package.json +2 -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
|
|
@@ -5980,7 +6010,46 @@ var HTTPError = class extends Error {
|
|
|
5980
6010
|
return Response.json(this.body, { status: this.status });
|
|
5981
6011
|
}
|
|
5982
6012
|
};
|
|
6013
|
+
const TRANSIENT_UPSTREAM_STATUSES = new Set([
|
|
6014
|
+
408,
|
|
6015
|
+
429,
|
|
6016
|
+
500,
|
|
6017
|
+
502,
|
|
6018
|
+
503,
|
|
6019
|
+
504,
|
|
6020
|
+
529
|
|
6021
|
+
]);
|
|
6022
|
+
/**
|
|
6023
|
+
* Upstream statuses worth retrying: request-scoped faults (timeouts, gateway
|
|
6024
|
+
* errors) plus capacity limits. Shared by `UpstreamRequestQueue` and the
|
|
6025
|
+
* Copilot token refresh so both agree on what "transient" means.
|
|
6026
|
+
*/
|
|
6027
|
+
function isTransientUpstreamStatus(status) {
|
|
6028
|
+
return TRANSIENT_UPSTREAM_STATUSES.has(status);
|
|
6029
|
+
}
|
|
6030
|
+
/**
|
|
6031
|
+
* Capacity signals that apply to the whole account or service rather than one
|
|
6032
|
+
* request. Only these warrant global back-pressure — applying a queue-wide
|
|
6033
|
+
* cooldown to a request-scoped 5xx turns one bad request into a proxy stall.
|
|
6034
|
+
*/
|
|
6035
|
+
function isCapacityLimitStatus(status) {
|
|
6036
|
+
return status === 429 || status === 529;
|
|
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
|
+
*/
|
|
5983
6047
|
function throwInvalidRequestError(message, param, code) {
|
|
6048
|
+
consola.warn("Rejected request", {
|
|
6049
|
+
param,
|
|
6050
|
+
code,
|
|
6051
|
+
message
|
|
6052
|
+
});
|
|
5984
6053
|
throw new HTTPError(400, { error: {
|
|
5985
6054
|
message,
|
|
5986
6055
|
type: "invalid_request_error",
|
|
@@ -5989,9 +6058,15 @@ function throwInvalidRequestError(message, param, code) {
|
|
|
5989
6058
|
} });
|
|
5990
6059
|
}
|
|
5991
6060
|
function fromTranslationFailure(failure) {
|
|
6061
|
+
consola.warn("Translation failed", {
|
|
6062
|
+
kind: failure.kind,
|
|
6063
|
+
status: failure.status,
|
|
6064
|
+
message: failure.message
|
|
6065
|
+
});
|
|
5992
6066
|
return new HTTPError(failure.status, { error: {
|
|
5993
6067
|
message: failure.message,
|
|
5994
|
-
type: "translation_error"
|
|
6068
|
+
type: "translation_error",
|
|
6069
|
+
...failure.kind ? { code: failure.kind } : {}
|
|
5995
6070
|
} });
|
|
5996
6071
|
}
|
|
5997
6072
|
function resolveModelOrThrow(modelId) {
|
|
@@ -6014,7 +6089,9 @@ function isStructuredErrorPayload(value) {
|
|
|
6014
6089
|
return typeof value === "object" && value !== null && "error" in value && typeof value.error === "object" && value.error !== null;
|
|
6015
6090
|
}
|
|
6016
6091
|
function upstreamErrorType(status) {
|
|
6017
|
-
|
|
6092
|
+
if (status === 429) return "rate_limit_error";
|
|
6093
|
+
if (status === 529) return "overloaded_error";
|
|
6094
|
+
return "upstream_error";
|
|
6018
6095
|
}
|
|
6019
6096
|
function createFallbackUpstreamError(message, response, rawText) {
|
|
6020
6097
|
return { error: {
|
|
@@ -6125,7 +6202,6 @@ function createResponsesEmulatorState(opts) {
|
|
|
6125
6202
|
const conversationHeadRecords = /* @__PURE__ */ new Map();
|
|
6126
6203
|
const inputItemRecords = /* @__PURE__ */ new Map();
|
|
6127
6204
|
const responseDeletionFlags = /* @__PURE__ */ new Map();
|
|
6128
|
-
const conversationDeletionFlags = /* @__PURE__ */ new Map();
|
|
6129
6205
|
const inputItemDeletionFlags = /* @__PURE__ */ new Map();
|
|
6130
6206
|
const allMaps = [
|
|
6131
6207
|
responseRecords,
|
|
@@ -6133,7 +6209,6 @@ function createResponsesEmulatorState(opts) {
|
|
|
6133
6209
|
conversationHeadRecords,
|
|
6134
6210
|
inputItemRecords,
|
|
6135
6211
|
responseDeletionFlags,
|
|
6136
|
-
conversationDeletionFlags,
|
|
6137
6212
|
inputItemDeletionFlags
|
|
6138
6213
|
];
|
|
6139
6214
|
let pruneIntervalId;
|
|
@@ -6201,7 +6276,6 @@ function createResponsesEmulatorState(opts) {
|
|
|
6201
6276
|
function deletionMap(kind) {
|
|
6202
6277
|
switch (kind) {
|
|
6203
6278
|
case "response": return responseDeletionFlags;
|
|
6204
|
-
case "conversation": return conversationDeletionFlags;
|
|
6205
6279
|
case "input_items": return inputItemDeletionFlags;
|
|
6206
6280
|
}
|
|
6207
6281
|
}
|
|
@@ -6211,7 +6285,6 @@ function createResponsesEmulatorState(opts) {
|
|
|
6211
6285
|
pruneMap(conversationHeadRecords, at);
|
|
6212
6286
|
pruneMap(inputItemRecords, at);
|
|
6213
6287
|
pruneMap(responseDeletionFlags, at);
|
|
6214
|
-
pruneMap(conversationDeletionFlags, at);
|
|
6215
6288
|
pruneMap(inputItemDeletionFlags, at);
|
|
6216
6289
|
}
|
|
6217
6290
|
function evictOldestFromLargestMap() {
|
|
@@ -6240,19 +6313,12 @@ function createResponsesEmulatorState(opts) {
|
|
|
6240
6313
|
}
|
|
6241
6314
|
}
|
|
6242
6315
|
return {
|
|
6243
|
-
isEnabled() {
|
|
6244
|
-
return configStore.isEmulatorEnabled();
|
|
6245
|
-
},
|
|
6246
|
-
getDefaultTtlSeconds() {
|
|
6247
|
-
return configStore.getEmulatorTtlSeconds();
|
|
6248
|
-
},
|
|
6249
6316
|
clear() {
|
|
6250
6317
|
responseRecords.clear();
|
|
6251
6318
|
conversationRecords.clear();
|
|
6252
6319
|
conversationHeadRecords.clear();
|
|
6253
6320
|
inputItemRecords.clear();
|
|
6254
6321
|
responseDeletionFlags.clear();
|
|
6255
|
-
conversationDeletionFlags.clear();
|
|
6256
6322
|
inputItemDeletionFlags.clear();
|
|
6257
6323
|
stopBackgroundPrune();
|
|
6258
6324
|
},
|
|
@@ -6267,7 +6333,7 @@ function createResponsesEmulatorState(opts) {
|
|
|
6267
6333
|
conversations: conversationRecords.size,
|
|
6268
6334
|
conversationHeads: conversationHeadRecords.size,
|
|
6269
6335
|
inputItems: inputItemRecords.size,
|
|
6270
|
-
deletions: responseDeletionFlags.size +
|
|
6336
|
+
deletions: responseDeletionFlags.size + inputItemDeletionFlags.size
|
|
6271
6337
|
};
|
|
6272
6338
|
},
|
|
6273
6339
|
setResponse(response, options) {
|
|
@@ -6276,7 +6342,6 @@ function createResponsesEmulatorState(opts) {
|
|
|
6276
6342
|
const conversationId = responseKeyFromConversation(response.conversation);
|
|
6277
6343
|
writeMap(conversationRecords, conversationId, response.conversation, options?.ttlSeconds);
|
|
6278
6344
|
writeMap(conversationHeadRecords, conversationId, response.id, options?.ttlSeconds);
|
|
6279
|
-
removeDeletionFlag(conversationDeletionFlags, conversationId);
|
|
6280
6345
|
}
|
|
6281
6346
|
return writeMap(responseRecords, response.id, response, options?.ttlSeconds);
|
|
6282
6347
|
},
|
|
@@ -6302,25 +6367,11 @@ function createResponsesEmulatorState(opts) {
|
|
|
6302
6367
|
};
|
|
6303
6368
|
},
|
|
6304
6369
|
setConversation(conversation, options) {
|
|
6305
|
-
|
|
6306
|
-
removeDeletionFlag(conversationDeletionFlags, conversationId);
|
|
6307
|
-
return writeMap(conversationRecords, conversationId, conversation, options?.ttlSeconds);
|
|
6370
|
+
return writeMap(conversationRecords, responseKeyFromConversation(conversation), conversation, options?.ttlSeconds);
|
|
6308
6371
|
},
|
|
6309
6372
|
getConversation(conversationId) {
|
|
6310
|
-
if (readDeletionFlag(conversationDeletionFlags, conversationId)) return;
|
|
6311
6373
|
return readMap(conversationRecords, conversationId);
|
|
6312
6374
|
},
|
|
6313
|
-
deleteConversation(conversationId, options) {
|
|
6314
|
-
pruneExpiredRecords();
|
|
6315
|
-
deleteMapEntry(conversationRecords, conversationId);
|
|
6316
|
-
deleteMapEntry(conversationHeadRecords, conversationId);
|
|
6317
|
-
putDeletionFlag(conversationDeletionFlags, conversationId, options?.ttlSeconds);
|
|
6318
|
-
return {
|
|
6319
|
-
id: conversationId,
|
|
6320
|
-
object: "conversation.deleted",
|
|
6321
|
-
deleted: true
|
|
6322
|
-
};
|
|
6323
|
-
},
|
|
6324
6375
|
setConversationHead(conversationId, responseId, options) {
|
|
6325
6376
|
return writeMap(conversationHeadRecords, conversationId, responseId, options?.ttlSeconds);
|
|
6326
6377
|
},
|
|
@@ -6335,16 +6386,6 @@ function createResponsesEmulatorState(opts) {
|
|
|
6335
6386
|
if (readDeletionFlag(inputItemDeletionFlags, responseId)) return;
|
|
6336
6387
|
return readMap(inputItemRecords, responseId);
|
|
6337
6388
|
},
|
|
6338
|
-
deleteInputItems(responseId, options) {
|
|
6339
|
-
pruneExpiredRecords();
|
|
6340
|
-
deleteMapEntry(inputItemRecords, responseId);
|
|
6341
|
-
putDeletionFlag(inputItemDeletionFlags, responseId, options?.ttlSeconds);
|
|
6342
|
-
return {
|
|
6343
|
-
id: responseId,
|
|
6344
|
-
object: "response.input_items.deleted",
|
|
6345
|
-
deleted: true
|
|
6346
|
-
};
|
|
6347
|
-
},
|
|
6348
6389
|
getDeletionFlag(kind, id) {
|
|
6349
6390
|
return readDeletionFlag(deletionMap(kind), id);
|
|
6350
6391
|
}
|
|
@@ -6592,7 +6633,7 @@ var CopilotClient = class {
|
|
|
6592
6633
|
const { response, release } = await this.request(path, errorMessage, {
|
|
6593
6634
|
method: "POST",
|
|
6594
6635
|
body: JSON.stringify(payload),
|
|
6595
|
-
retryable:
|
|
6636
|
+
retryable: "capacity",
|
|
6596
6637
|
...options
|
|
6597
6638
|
});
|
|
6598
6639
|
if (payload.stream) return withRelease(events(response), release);
|
|
@@ -6626,10 +6667,11 @@ var CopilotClient = class {
|
|
|
6626
6667
|
}
|
|
6627
6668
|
});
|
|
6628
6669
|
}
|
|
6629
|
-
async createEmbeddings(payload) {
|
|
6670
|
+
async createEmbeddings(payload, options) {
|
|
6630
6671
|
return this.requestJson("/embeddings", "Failed to create embeddings", {
|
|
6631
6672
|
method: "POST",
|
|
6632
6673
|
body: JSON.stringify(payload),
|
|
6674
|
+
signal: options?.signal,
|
|
6633
6675
|
retryable: true
|
|
6634
6676
|
});
|
|
6635
6677
|
}
|
|
@@ -6890,6 +6932,27 @@ function normalizeGheDomain(input) {
|
|
|
6890
6932
|
return domain;
|
|
6891
6933
|
}
|
|
6892
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
|
+
/**
|
|
6893
6956
|
* Build GitHub base URL and API base URL for a given GHE domain,
|
|
6894
6957
|
* or return the public GitHub defaults when no domain is provided.
|
|
6895
6958
|
*/
|
|
@@ -6957,8 +7020,10 @@ var UpstreamRequestQueue = class {
|
|
|
6957
7020
|
lease.release();
|
|
6958
7021
|
throw error;
|
|
6959
7022
|
}
|
|
6960
|
-
|
|
6961
|
-
|
|
7023
|
+
const { status } = response;
|
|
7024
|
+
const isCapacityLimit = isCapacityLimitStatus(status);
|
|
7025
|
+
if (!((context.retryable === "capacity" ? isCapacityLimit : context.retryable === true && isTransientUpstreamStatus(status)) && attempt < this.options.maxRetries)) {
|
|
7026
|
+
if (isCapacityLimit) this.applyCooldown(this.getRetryDelayMs(response, 0));
|
|
6962
7027
|
return {
|
|
6963
7028
|
response,
|
|
6964
7029
|
release: lease.release
|
|
@@ -6966,10 +7031,10 @@ var UpstreamRequestQueue = class {
|
|
|
6966
7031
|
}
|
|
6967
7032
|
const delayMs = this.getRetryDelayMs(response, attempt);
|
|
6968
7033
|
await discardResponse(response);
|
|
7034
|
+
if (isCapacityLimit) this.applyCooldown(delayMs);
|
|
6969
7035
|
lease.release();
|
|
6970
|
-
this.applyCooldown(delayMs);
|
|
6971
7036
|
this.logger.warn([
|
|
6972
|
-
|
|
7037
|
+
`Upstream ${status};`,
|
|
6973
7038
|
`retrying ${formatRequestContext(context)}`,
|
|
6974
7039
|
`in ${formatDurationMs(delayMs)}`,
|
|
6975
7040
|
`(attempt ${attempt + 1}/${this.options.maxRetries})`
|
|
@@ -7244,16 +7309,8 @@ async function setupGitHubToken(options) {
|
|
|
7244
7309
|
function isAuthError(error) {
|
|
7245
7310
|
return error instanceof HTTPError && (error.status === 401 || error.status === 403);
|
|
7246
7311
|
}
|
|
7247
|
-
const TRANSIENT_HTTP_STATUSES = new Set([
|
|
7248
|
-
408,
|
|
7249
|
-
429,
|
|
7250
|
-
500,
|
|
7251
|
-
502,
|
|
7252
|
-
503,
|
|
7253
|
-
504
|
|
7254
|
-
]);
|
|
7255
7312
|
function isTransientHttpError(error) {
|
|
7256
|
-
return
|
|
7313
|
+
return isTransientUpstreamStatus(error.status);
|
|
7257
7314
|
}
|
|
7258
7315
|
async function logUser() {
|
|
7259
7316
|
const user = await createGitHubClient().getGitHubUser();
|
|
@@ -7291,9 +7348,7 @@ async function runAuth(options) {
|
|
|
7291
7348
|
authStore.showToken = options.showToken;
|
|
7292
7349
|
await ensurePaths();
|
|
7293
7350
|
await readConfig();
|
|
7294
|
-
authStore
|
|
7295
|
-
if (options.gheDomain !== void 0) authStore.gheDomain = options.gheDomain ? normalizeGheDomain(options.gheDomain) : void 0;
|
|
7296
|
-
if (authStore.gheDomain && authStore.accountType === "individual") authStore.accountType = "enterprise";
|
|
7351
|
+
applyGheDomain(authStore, getCachedConfig().gheDomain, options.gheDomain);
|
|
7297
7352
|
await cacheVSCodeVersion();
|
|
7298
7353
|
await setupGitHubToken({ force: true });
|
|
7299
7354
|
consola.success("GitHub token written to config.json");
|
|
@@ -7339,7 +7394,7 @@ const checkUsage = defineCommand({
|
|
|
7339
7394
|
async run() {
|
|
7340
7395
|
await ensurePaths();
|
|
7341
7396
|
await readConfig();
|
|
7342
|
-
authStore
|
|
7397
|
+
applyGheDomain(authStore, getCachedConfig().gheDomain);
|
|
7343
7398
|
await cacheVSCodeVersion();
|
|
7344
7399
|
await setupGitHubToken();
|
|
7345
7400
|
try {
|
|
@@ -7364,7 +7419,7 @@ const checkUsage = defineCommand({
|
|
|
7364
7419
|
});
|
|
7365
7420
|
//#endregion
|
|
7366
7421
|
//#region src/util/version.ts
|
|
7367
|
-
const VERSION = "0.
|
|
7422
|
+
const VERSION = "0.9.0";
|
|
7368
7423
|
//#endregion
|
|
7369
7424
|
//#region src/debug.ts
|
|
7370
7425
|
function getRuntimeInfo() {
|
|
@@ -47896,6 +47951,20 @@ function inferReasoningEffort(budgetTokens) {
|
|
|
47896
47951
|
if (budgetTokens <= 24e3) return "medium";
|
|
47897
47952
|
return "high";
|
|
47898
47953
|
}
|
|
47954
|
+
/**
|
|
47955
|
+
* The effort to send upstream: the level the caller named, else one inferred
|
|
47956
|
+
* from the thinking budget.
|
|
47957
|
+
*
|
|
47958
|
+
* `output_config.effort` used to be dropped entirely on this path — it is not
|
|
47959
|
+
* part of the OpenAI chat schema, so the normalizer never read it — which
|
|
47960
|
+
* silently downgraded a caller asking for `max` to whatever the budget
|
|
47961
|
+
* heuristic produced (`high` at most). Copilot does accept `reasoning_effort`
|
|
47962
|
+
* here: probed 2026-07-26, every reasoning Claude model on `/chat/completions`
|
|
47963
|
+
* accepts the levels it advertises, `max` included.
|
|
47964
|
+
*/
|
|
47965
|
+
function resolveRequestEffort(request, budgetTokens) {
|
|
47966
|
+
return request.outputEffort ?? inferReasoningEffort(budgetTokens);
|
|
47967
|
+
}
|
|
47899
47968
|
function inferModelFamily(model) {
|
|
47900
47969
|
if (model.startsWith("claude")) return "claude";
|
|
47901
47970
|
if (model.startsWith("gpt") || model.startsWith("o1") || model.startsWith("o3") || model.startsWith("o4")) return "gpt";
|
|
@@ -47909,8 +47978,9 @@ const baseProfile = {
|
|
|
47909
47978
|
includeUsageOnStream: true,
|
|
47910
47979
|
applyThinking(request) {
|
|
47911
47980
|
const thinking = request.thinking;
|
|
47912
|
-
if (
|
|
47913
|
-
|
|
47981
|
+
if (thinking?.type === "disabled") return {};
|
|
47982
|
+
if (!thinking) return request.outputEffort ? { reasoning_effort: request.outputEffort } : {};
|
|
47983
|
+
return { reasoning_effort: resolveRequestEffort(request, thinking.type === "adaptive" ? 24e3 : thinking.budgetTokens) };
|
|
47914
47984
|
}
|
|
47915
47985
|
};
|
|
47916
47986
|
const claudeProfile = {
|
|
@@ -47920,10 +47990,11 @@ const claudeProfile = {
|
|
|
47920
47990
|
includeUsageOnStream: true,
|
|
47921
47991
|
applyThinking(request) {
|
|
47922
47992
|
const thinking = request.thinking;
|
|
47923
|
-
if (
|
|
47993
|
+
if (thinking?.type === "disabled") return {};
|
|
47994
|
+
if (!thinking) return request.outputEffort ? { reasoning_effort: request.outputEffort } : {};
|
|
47924
47995
|
const budgetTokens = thinking.type === "adaptive" ? 24e3 : thinking.budgetTokens;
|
|
47925
47996
|
return {
|
|
47926
|
-
reasoning_effort:
|
|
47997
|
+
reasoning_effort: resolveRequestEffort(request, budgetTokens),
|
|
47927
47998
|
thinking_budget: budgetTokens
|
|
47928
47999
|
};
|
|
47929
48000
|
}
|
|
@@ -48448,6 +48519,7 @@ function buildCapiExecutionPlan(request, options = {}) {
|
|
|
48448
48519
|
stream: request.stream,
|
|
48449
48520
|
temperature: request.temperature,
|
|
48450
48521
|
top_p: request.topP,
|
|
48522
|
+
...request.topK != null ? { top_k: request.topK } : {},
|
|
48451
48523
|
user: request.userId,
|
|
48452
48524
|
tools: serializeTools(request.tools),
|
|
48453
48525
|
tool_choice: serializeToolChoice(request.toolChoice),
|
|
@@ -48706,6 +48778,12 @@ const anthropicThinkingSchema = union([
|
|
|
48706
48778
|
budget_tokens: number().int().positive()
|
|
48707
48779
|
}).loose()
|
|
48708
48780
|
]);
|
|
48781
|
+
/**
|
|
48782
|
+
* `format` stays strict: an unrecognized key here means the caller expects a
|
|
48783
|
+
* constraint the proxy cannot translate, and silently accepting it would let a
|
|
48784
|
+
* schema-constrained request come back unconstrained. Rejecting is the honest
|
|
48785
|
+
* answer — see docs/solutions/integration-issues/claude-code-messages-startup-payloads.md.
|
|
48786
|
+
*/
|
|
48709
48787
|
const anthropicOutputFormatSchema = object({
|
|
48710
48788
|
type: literal("json_schema"),
|
|
48711
48789
|
schema: jsonObjectSchema,
|
|
@@ -48713,16 +48791,24 @@ const anthropicOutputFormatSchema = object({
|
|
|
48713
48791
|
description: string().nullable().optional(),
|
|
48714
48792
|
strict: boolean().optional()
|
|
48715
48793
|
}).strict();
|
|
48794
|
+
/**
|
|
48795
|
+
* `output_config` itself is loose. It is the fastest-moving object in the
|
|
48796
|
+
* Anthropic Messages schema, and it was the only strict container on this
|
|
48797
|
+
* boundary — so every field Anthropic added arrived here as a local 400 before
|
|
48798
|
+
* the request could reach a model that may well accept it. Unknown keys are
|
|
48799
|
+
* forwarded rather than rejected; `format` keeps its own strict contract above,
|
|
48800
|
+
* and `sanitizeOutputConfig` preserves the extras rather than dropping them.
|
|
48801
|
+
*/
|
|
48716
48802
|
const anthropicOutputConfigSchema = object({
|
|
48717
48803
|
effort: _enum([
|
|
48718
48804
|
"low",
|
|
48719
48805
|
"medium",
|
|
48720
48806
|
"high",
|
|
48721
|
-
"
|
|
48722
|
-
"
|
|
48807
|
+
"xhigh",
|
|
48808
|
+
"max"
|
|
48723
48809
|
]).nullable().optional(),
|
|
48724
48810
|
format: anthropicOutputFormatSchema.optional()
|
|
48725
|
-
}).
|
|
48811
|
+
}).loose();
|
|
48726
48812
|
const anthropicMessagesBasePayloadSchema = object({
|
|
48727
48813
|
model: string().min(1),
|
|
48728
48814
|
messages: array(anthropicMessageSchema).min(1),
|
|
@@ -48767,11 +48853,23 @@ function parseEmbeddingRequest(payload) {
|
|
|
48767
48853
|
}
|
|
48768
48854
|
//#endregion
|
|
48769
48855
|
//#region src/types/copilot.ts
|
|
48856
|
+
/**
|
|
48857
|
+
* Effort levels Copilot accepts on `reasoning_effort`.
|
|
48858
|
+
*
|
|
48859
|
+
* Probed 2026-07-26 (`scripts/probes/effort-and-tokens.ts`): a model accepts
|
|
48860
|
+
* exactly the levels it advertises in `capabilities.supports.reasoning_effort`
|
|
48861
|
+
* and rejects the rest with 400, so always clamp against that list rather than
|
|
48862
|
+
* assuming this union is universally valid. `max` is gpt-5.6-and-later on the
|
|
48863
|
+
* Responses boundary, but available across the Claude family.
|
|
48864
|
+
*/
|
|
48770
48865
|
const REASONING_EFFORT_VALUES = [
|
|
48866
|
+
"none",
|
|
48771
48867
|
"minimal",
|
|
48772
48868
|
"low",
|
|
48773
48869
|
"medium",
|
|
48774
|
-
"high"
|
|
48870
|
+
"high",
|
|
48871
|
+
"xhigh",
|
|
48872
|
+
"max"
|
|
48775
48873
|
];
|
|
48776
48874
|
//#endregion
|
|
48777
48875
|
//#region src/ingest/validation/openai-chat.ts
|
|
@@ -48885,9 +48983,19 @@ function parseOpenAIChatPayload(payload) {
|
|
|
48885
48983
|
}
|
|
48886
48984
|
//#endregion
|
|
48887
48985
|
//#region src/ingest/validation/responses.ts
|
|
48986
|
+
/**
|
|
48987
|
+
* Marks where a reusable cache prefix ends. Only models with explicit prompt
|
|
48988
|
+
* caching accept it — probed 2026-07-26, gpt-5.6 returns 200 while gpt-5.5 and
|
|
48989
|
+
* earlier return `400 prompt_cache_breakpoint is not supported on this model`
|
|
48990
|
+
* (`scripts/probes/prompt-caching.ts`).
|
|
48991
|
+
*
|
|
48992
|
+
* `explicit` is the only documented mode.
|
|
48993
|
+
*/
|
|
48994
|
+
const promptCacheBreakpointSchema = object({ mode: literal("explicit") }).loose();
|
|
48888
48995
|
const responsesInputTextSchema = object({
|
|
48889
48996
|
type: _enum(["input_text", "output_text"]),
|
|
48890
|
-
text: string()
|
|
48997
|
+
text: string(),
|
|
48998
|
+
prompt_cache_breakpoint: promptCacheBreakpointSchema.nullable().optional()
|
|
48891
48999
|
}).loose();
|
|
48892
49000
|
const responsesInputImageSchema = object({
|
|
48893
49001
|
type: literal("input_image"),
|
|
@@ -48898,7 +49006,8 @@ const responsesInputImageSchema = object({
|
|
|
48898
49006
|
"high",
|
|
48899
49007
|
"auto",
|
|
48900
49008
|
"original"
|
|
48901
|
-
]).optional()
|
|
49009
|
+
]).optional(),
|
|
49010
|
+
prompt_cache_breakpoint: promptCacheBreakpointSchema.nullable().optional()
|
|
48902
49011
|
}).loose().superRefine((item, ctx) => {
|
|
48903
49012
|
if (!item.image_url && !item.file_id) ctx.addIssue({
|
|
48904
49013
|
code: "custom",
|
|
@@ -49076,14 +49185,7 @@ const responsesToolChoiceSchema = union([
|
|
|
49076
49185
|
}).loose()
|
|
49077
49186
|
]);
|
|
49078
49187
|
const responsesReasoningConfigSchema = object({
|
|
49079
|
-
effort: _enum(
|
|
49080
|
-
"none",
|
|
49081
|
-
"minimal",
|
|
49082
|
-
"low",
|
|
49083
|
-
"medium",
|
|
49084
|
-
"high",
|
|
49085
|
-
"xhigh"
|
|
49086
|
-
]).nullable().optional(),
|
|
49188
|
+
effort: _enum(REASONING_EFFORT_VALUES).nullable().optional(),
|
|
49087
49189
|
generate_summary: _enum([
|
|
49088
49190
|
"auto",
|
|
49089
49191
|
"concise",
|
|
@@ -49141,6 +49243,10 @@ function createResponsesPayloadSchema(options) {
|
|
|
49141
49243
|
stream_options: object({ include_obfuscation: boolean().nullable().optional() }).loose().nullable().optional(),
|
|
49142
49244
|
safety_identifier: string().nullable().optional(),
|
|
49143
49245
|
prompt_cache_key: string().nullable().optional(),
|
|
49246
|
+
prompt_cache_options: object({
|
|
49247
|
+
mode: _enum(["implicit", "explicit"]).optional(),
|
|
49248
|
+
ttl: literal("30m").optional()
|
|
49249
|
+
}).loose().nullable().optional(),
|
|
49144
49250
|
prompt_cache_retention: _enum(["in-memory", "24h"]).nullable().optional(),
|
|
49145
49251
|
truncation: _enum(["auto", "disabled"]).nullable().optional(),
|
|
49146
49252
|
parallel_tool_calls: boolean().nullable().optional(),
|
|
@@ -49317,154 +49423,9 @@ function createUpstreamSignalFromConfig(clientSignal) {
|
|
|
49317
49423
|
return createUpstreamSignal(clientSignal, authStore.upstreamTimeoutSeconds !== void 0 ? authStore.upstreamTimeoutSeconds * 1e3 : void 0);
|
|
49318
49424
|
}
|
|
49319
49425
|
//#endregion
|
|
49320
|
-
//#region src/pipeline/runner.ts
|
|
49321
|
-
async function runPipeline(params, config) {
|
|
49322
|
-
const ingested = protocolRegistry.ingest(config.protocol, params.body, params.headers);
|
|
49323
|
-
const meta = ingested.meta;
|
|
49324
|
-
const payload = config.afterIngest ? config.afterIngest({
|
|
49325
|
-
payload: ingested.payload,
|
|
49326
|
-
meta,
|
|
49327
|
-
headers: params.headers
|
|
49328
|
-
}) : ingested.payload;
|
|
49329
|
-
const transformResult = config.transformChain.apply({
|
|
49330
|
-
model: payload.model,
|
|
49331
|
-
payload,
|
|
49332
|
-
meta: { betaHeaders: meta.betaHeaders }
|
|
49333
|
-
});
|
|
49334
|
-
payload.model = transformResult.model;
|
|
49335
|
-
const selectedModel = transformResult.resolvedModel;
|
|
49336
|
-
const modelMapping = {
|
|
49337
|
-
originalModel: transformResult.trace.length > 0 ? transformResult.trace[0].from : payload.model,
|
|
49338
|
-
steps: transformResult.trace.map((r) => ({
|
|
49339
|
-
tag: r.tag,
|
|
49340
|
-
from: r.from,
|
|
49341
|
-
to: r.to
|
|
49342
|
-
}))
|
|
49343
|
-
};
|
|
49344
|
-
if (config.afterTransform) await config.afterTransform({
|
|
49345
|
-
payload,
|
|
49346
|
-
meta,
|
|
49347
|
-
headers: params.headers,
|
|
49348
|
-
transformResult,
|
|
49349
|
-
selectedModel
|
|
49350
|
-
});
|
|
49351
|
-
const upstreamSignal = createUpstreamSignalFromConfig(params.signal);
|
|
49352
|
-
const copilotClient = createCopilotClient();
|
|
49353
|
-
const ctx = config.buildStrategyContext({
|
|
49354
|
-
payload,
|
|
49355
|
-
meta,
|
|
49356
|
-
headers: params.headers,
|
|
49357
|
-
selectedModel,
|
|
49358
|
-
copilotClient,
|
|
49359
|
-
upstreamSignal,
|
|
49360
|
-
modelMapping
|
|
49361
|
-
});
|
|
49362
|
-
return {
|
|
49363
|
-
result: await config.strategyRegistry.select(selectedModel, ctx).execute(ctx),
|
|
49364
|
-
modelMapping
|
|
49365
|
-
};
|
|
49366
|
-
}
|
|
49367
|
-
//#endregion
|
|
49368
|
-
//#region src/transform/chain.ts
|
|
49369
|
-
function composeModelTransforms(...steps) {
|
|
49370
|
-
return { apply(input) {
|
|
49371
|
-
let current = input.model;
|
|
49372
|
-
const trace = [];
|
|
49373
|
-
let resolvedModel = input.resolvedModel;
|
|
49374
|
-
const payload = input.payload;
|
|
49375
|
-
for (const step of steps) {
|
|
49376
|
-
const output = step.apply({
|
|
49377
|
-
...input,
|
|
49378
|
-
model: current,
|
|
49379
|
-
payload,
|
|
49380
|
-
resolvedModel
|
|
49381
|
-
});
|
|
49382
|
-
if (output === null) continue;
|
|
49383
|
-
const from = current;
|
|
49384
|
-
const to = output.model;
|
|
49385
|
-
if (output.mutatePayload) output.mutatePayload(payload);
|
|
49386
|
-
if (output.tag) trace.push({
|
|
49387
|
-
tag: output.tag,
|
|
49388
|
-
from,
|
|
49389
|
-
to
|
|
49390
|
-
});
|
|
49391
|
-
current = to;
|
|
49392
|
-
if (output.resolvedModel !== void 0) resolvedModel = output.resolvedModel;
|
|
49393
|
-
}
|
|
49394
|
-
if (resolvedModel === void 0) resolvedModel = modelCache.findById(current);
|
|
49395
|
-
return {
|
|
49396
|
-
model: current,
|
|
49397
|
-
resolvedModel,
|
|
49398
|
-
trace
|
|
49399
|
-
};
|
|
49400
|
-
} };
|
|
49401
|
-
}
|
|
49402
|
-
//#endregion
|
|
49403
49426
|
//#region src/transform/constants.ts
|
|
49404
49427
|
const CONTEXT_BETA_RE = /^context-\d+[km]-/;
|
|
49405
49428
|
//#endregion
|
|
49406
|
-
//#region src/transform/request-model-policy.ts
|
|
49407
|
-
const COMPACT_SYSTEM_PROMPT_START = "You are a helpful AI assistant tasked with summarizing conversations";
|
|
49408
|
-
function applyMessagesModelPolicy(payload, options) {
|
|
49409
|
-
const originalModel = payload.model;
|
|
49410
|
-
if (options?.betaUpgraded) return {
|
|
49411
|
-
originalModel,
|
|
49412
|
-
routedModel: originalModel
|
|
49413
|
-
};
|
|
49414
|
-
const smallModel = configStore.getSmallModel();
|
|
49415
|
-
if (!smallModel || !configStore.isCompactSmallModelEnabled() || !isCompactRequest(payload)) return {
|
|
49416
|
-
originalModel,
|
|
49417
|
-
routedModel: originalModel
|
|
49418
|
-
};
|
|
49419
|
-
if (canRouteToSmallModel(payload, modelCache.findById(originalModel), modelCache.findById(smallModel))) {
|
|
49420
|
-
payload.model = smallModel;
|
|
49421
|
-
return {
|
|
49422
|
-
originalModel,
|
|
49423
|
-
routedModel: smallModel,
|
|
49424
|
-
reason: "compact"
|
|
49425
|
-
};
|
|
49426
|
-
}
|
|
49427
|
-
return {
|
|
49428
|
-
originalModel,
|
|
49429
|
-
routedModel: originalModel
|
|
49430
|
-
};
|
|
49431
|
-
}
|
|
49432
|
-
function isCompactRequest(payload) {
|
|
49433
|
-
if (typeof payload.system === "string") return payload.system.startsWith(COMPACT_SYSTEM_PROMPT_START);
|
|
49434
|
-
if (!Array.isArray(payload.system)) return false;
|
|
49435
|
-
return payload.system.some((block) => typeof block.text === "string" && block.text.startsWith(COMPACT_SYSTEM_PROMPT_START));
|
|
49436
|
-
}
|
|
49437
|
-
function canRouteToSmallModel(payload, originalModel, smallModel) {
|
|
49438
|
-
if (!originalModel || !smallModel) return false;
|
|
49439
|
-
const originalEndpoints = new Set(originalModel.supported_endpoints ?? []);
|
|
49440
|
-
const smallEndpoints = new Set(smallModel.supported_endpoints ?? []);
|
|
49441
|
-
for (const endpoint of originalEndpoints) if (!smallEndpoints.has(endpoint)) return false;
|
|
49442
|
-
if (payload.tools?.length && !(smallModel.capabilities.supports.tool_calls ?? false)) return false;
|
|
49443
|
-
if (payload.thinking && !(smallModel.capabilities.supports.adaptive_thinking ?? false)) return false;
|
|
49444
|
-
if (hasVisionInput$1(payload) && !(smallModel.capabilities.supports.vision ?? false)) return false;
|
|
49445
|
-
return true;
|
|
49446
|
-
}
|
|
49447
|
-
function hasVisionInput$1(payload) {
|
|
49448
|
-
return payload.messages.some((message) => containsVisionContent$1(message.content));
|
|
49449
|
-
}
|
|
49450
|
-
function containsVisionContent$1(content) {
|
|
49451
|
-
if (!Array.isArray(content)) return false;
|
|
49452
|
-
return content.some((block) => block.type === "image");
|
|
49453
|
-
}
|
|
49454
|
-
//#endregion
|
|
49455
|
-
//#region src/transform/policy.ts
|
|
49456
|
-
const modelPolicyStep = {
|
|
49457
|
-
tag: "POLICY",
|
|
49458
|
-
apply({ payload, meta }) {
|
|
49459
|
-
const routing = applyMessagesModelPolicy(payload, { betaUpgraded: meta?.betaHeaders?.some((b) => CONTEXT_BETA_RE.test(b)) ?? false });
|
|
49460
|
-
if (!routing.reason) return null;
|
|
49461
|
-
return {
|
|
49462
|
-
model: routing.routedModel,
|
|
49463
|
-
tag: "COMPACT"
|
|
49464
|
-
};
|
|
49465
|
-
}
|
|
49466
|
-
};
|
|
49467
|
-
//#endregion
|
|
49468
49429
|
//#region src/transform/model-rewrite.ts
|
|
49469
49430
|
/**
|
|
49470
49431
|
* Unified model rewrite: user rules → built-in normalization → pass-through.
|
|
@@ -49521,42 +49482,133 @@ function matchesGlob(pattern, value) {
|
|
|
49521
49482
|
return new RegExp(`^${pattern.replace(GLOB_SPECIAL_RE, "\\$&").replace(GLOB_STAR_RE, ".*")}$`).test(value);
|
|
49522
49483
|
}
|
|
49523
49484
|
//#endregion
|
|
49524
|
-
//#region src/transform/
|
|
49525
|
-
const
|
|
49526
|
-
|
|
49527
|
-
|
|
49528
|
-
|
|
49529
|
-
|
|
49530
|
-
|
|
49531
|
-
|
|
49532
|
-
|
|
49533
|
-
|
|
49534
|
-
|
|
49535
|
-
|
|
49536
|
-
|
|
49537
|
-
|
|
49485
|
+
//#region src/transform/request-model-policy.ts
|
|
49486
|
+
const COMPACT_SYSTEM_PROMPT_START = "You are a helpful AI assistant tasked with summarizing conversations";
|
|
49487
|
+
function applyMessagesModelPolicy(payload, options) {
|
|
49488
|
+
const originalModel = payload.model;
|
|
49489
|
+
if (options?.betaUpgraded) return {
|
|
49490
|
+
originalModel,
|
|
49491
|
+
routedModel: originalModel
|
|
49492
|
+
};
|
|
49493
|
+
const smallModel = configStore.getSmallModel();
|
|
49494
|
+
if (!smallModel || !configStore.isCompactSmallModelEnabled() || !isCompactRequest(payload)) return {
|
|
49495
|
+
originalModel,
|
|
49496
|
+
routedModel: originalModel
|
|
49497
|
+
};
|
|
49498
|
+
if (canRouteToSmallModel(payload, modelCache.findById(originalModel), modelCache.findById(smallModel))) {
|
|
49499
|
+
payload.model = smallModel;
|
|
49538
49500
|
return {
|
|
49539
|
-
|
|
49540
|
-
|
|
49541
|
-
|
|
49542
|
-
p.model = result.model;
|
|
49543
|
-
}
|
|
49501
|
+
originalModel,
|
|
49502
|
+
routedModel: smallModel,
|
|
49503
|
+
reason: "compact"
|
|
49544
49504
|
};
|
|
49545
49505
|
}
|
|
49546
|
-
|
|
49506
|
+
return {
|
|
49507
|
+
originalModel,
|
|
49508
|
+
routedModel: originalModel
|
|
49509
|
+
};
|
|
49510
|
+
}
|
|
49511
|
+
function isCompactRequest(payload) {
|
|
49512
|
+
if (typeof payload.system === "string") return payload.system.startsWith(COMPACT_SYSTEM_PROMPT_START);
|
|
49513
|
+
if (!Array.isArray(payload.system)) return false;
|
|
49514
|
+
return payload.system.some((block) => typeof block.text === "string" && block.text.startsWith(COMPACT_SYSTEM_PROMPT_START));
|
|
49515
|
+
}
|
|
49516
|
+
function canRouteToSmallModel(payload, originalModel, smallModel) {
|
|
49517
|
+
if (!originalModel || !smallModel) return false;
|
|
49518
|
+
const originalEndpoints = new Set(originalModel.supported_endpoints ?? []);
|
|
49519
|
+
const smallEndpoints = new Set(smallModel.supported_endpoints ?? []);
|
|
49520
|
+
for (const endpoint of originalEndpoints) if (!smallEndpoints.has(endpoint)) return false;
|
|
49521
|
+
if (payload.tools?.length && !(smallModel.capabilities.supports.tool_calls ?? false)) return false;
|
|
49522
|
+
if (payload.thinking && !(smallModel.capabilities.supports.adaptive_thinking ?? false)) return false;
|
|
49523
|
+
if (hasVisionInput$1(payload) && !(smallModel.capabilities.supports.vision ?? false)) return false;
|
|
49524
|
+
return true;
|
|
49525
|
+
}
|
|
49526
|
+
function hasVisionInput$1(payload) {
|
|
49527
|
+
return payload.messages.some((message) => containsVisionContent$1(message.content));
|
|
49528
|
+
}
|
|
49529
|
+
function containsVisionContent$1(content) {
|
|
49530
|
+
if (!Array.isArray(content)) return false;
|
|
49531
|
+
return content.some((block) => block.type === "image");
|
|
49532
|
+
}
|
|
49547
49533
|
//#endregion
|
|
49548
|
-
//#region src/transform/
|
|
49549
|
-
|
|
49550
|
-
|
|
49551
|
-
|
|
49552
|
-
|
|
49553
|
-
|
|
49554
|
-
|
|
49555
|
-
|
|
49556
|
-
|
|
49557
|
-
|
|
49534
|
+
//#region src/transform/resolve-model.ts
|
|
49535
|
+
/**
|
|
49536
|
+
* Resolve the model a request should be dispatched with.
|
|
49537
|
+
*
|
|
49538
|
+
* Applies, in order:
|
|
49539
|
+
* 1. model rewrite -- user `modelRewrites` rules, then dash/dot normalization
|
|
49540
|
+
* 2. compact small-model routing -- only when `applyPolicy` is set
|
|
49541
|
+
* 3. lookup against the cached Copilot model list
|
|
49542
|
+
*
|
|
49543
|
+
* Order is load-bearing: policy inspects `payload.model`, so the rewrite must
|
|
49544
|
+
* be written back to the payload before policy runs. Both steps mutate
|
|
49545
|
+
* `payload.model` directly, which is also what dispatch reads downstream.
|
|
49546
|
+
*/
|
|
49547
|
+
function resolveRequestModel({ payload, betaHeaders, applyPolicy }) {
|
|
49548
|
+
const originalModel = payload.model;
|
|
49549
|
+
const steps = [];
|
|
49550
|
+
const rewrite = applyModelRewrite(payload);
|
|
49551
|
+
if (rewrite.reason) steps.push({
|
|
49552
|
+
tag: rewrite.reason,
|
|
49553
|
+
from: originalModel,
|
|
49554
|
+
to: rewrite.model
|
|
49555
|
+
});
|
|
49556
|
+
if (applyPolicy) {
|
|
49557
|
+
const routing = applyMessagesModelPolicy(payload, { betaUpgraded: betaHeaders?.some((b) => CONTEXT_BETA_RE.test(b)) ?? false });
|
|
49558
|
+
if (routing.reason) {
|
|
49559
|
+
steps.push({
|
|
49560
|
+
tag: "COMPACT",
|
|
49561
|
+
from: routing.originalModel,
|
|
49562
|
+
to: routing.routedModel
|
|
49563
|
+
});
|
|
49564
|
+
payload.model = routing.routedModel;
|
|
49565
|
+
}
|
|
49558
49566
|
}
|
|
49559
|
-
return
|
|
49567
|
+
return {
|
|
49568
|
+
model: payload.model,
|
|
49569
|
+
resolvedModel: modelCache.findById(payload.model),
|
|
49570
|
+
modelMapping: {
|
|
49571
|
+
originalModel,
|
|
49572
|
+
steps
|
|
49573
|
+
}
|
|
49574
|
+
};
|
|
49575
|
+
}
|
|
49576
|
+
//#endregion
|
|
49577
|
+
//#region src/pipeline/runner.ts
|
|
49578
|
+
async function runPipeline(params, config) {
|
|
49579
|
+
const ingested = protocolRegistry.ingest(config.protocol, params.body, params.headers);
|
|
49580
|
+
const meta = ingested.meta;
|
|
49581
|
+
const payload = config.afterIngest ? config.afterIngest({
|
|
49582
|
+
payload: ingested.payload,
|
|
49583
|
+
meta,
|
|
49584
|
+
headers: params.headers
|
|
49585
|
+
}) : ingested.payload;
|
|
49586
|
+
const { resolvedModel: selectedModel, modelMapping } = resolveRequestModel({
|
|
49587
|
+
payload,
|
|
49588
|
+
betaHeaders: meta.betaHeaders,
|
|
49589
|
+
applyPolicy: config.applyModelPolicy
|
|
49590
|
+
});
|
|
49591
|
+
if (config.afterTransform) await config.afterTransform({
|
|
49592
|
+
payload,
|
|
49593
|
+
meta,
|
|
49594
|
+
headers: params.headers,
|
|
49595
|
+
selectedModel
|
|
49596
|
+
});
|
|
49597
|
+
const upstreamSignal = createUpstreamSignalFromConfig(params.signal);
|
|
49598
|
+
const copilotClient = createCopilotClient();
|
|
49599
|
+
const ctx = config.buildStrategyContext({
|
|
49600
|
+
payload,
|
|
49601
|
+
meta,
|
|
49602
|
+
headers: params.headers,
|
|
49603
|
+
selectedModel,
|
|
49604
|
+
copilotClient,
|
|
49605
|
+
upstreamSignal,
|
|
49606
|
+
modelMapping
|
|
49607
|
+
});
|
|
49608
|
+
return {
|
|
49609
|
+
result: await config.strategyRegistry.select(selectedModel, ctx).execute(ctx),
|
|
49610
|
+
modelMapping
|
|
49611
|
+
};
|
|
49560
49612
|
}
|
|
49561
49613
|
//#endregion
|
|
49562
49614
|
//#region src/translator/responses/signature-codec.ts
|
|
@@ -49628,7 +49680,28 @@ function isOutputConfigEffort(value) {
|
|
|
49628
49680
|
return OUTPUT_CONFIG_EFFORT_RANK.has(value);
|
|
49629
49681
|
}
|
|
49630
49682
|
function normalizeOutputConfigEffort(effort, model) {
|
|
49631
|
-
|
|
49683
|
+
return clampEffortToAdvertised(effort, model?.capabilities.supports.reasoning_effort);
|
|
49684
|
+
}
|
|
49685
|
+
/**
|
|
49686
|
+
* Clamp an effort to the highest level a model actually advertises.
|
|
49687
|
+
*
|
|
49688
|
+
* Probed 2026-07-26 (`scripts/probes/effort-and-tokens.ts`): a model rejects
|
|
49689
|
+
* every level it does not advertise, and the levels are NOT an ordered ladder
|
|
49690
|
+
* every model implements a prefix of — `claude-opus-4.6` and
|
|
49691
|
+
* `claude-sonnet-4.6` advertise `max` but not `xhigh`, and reject `xhigh` while
|
|
49692
|
+
* accepting `max`. So the target must be derived from the advertised list, not
|
|
49693
|
+
* from a fixed fallback.
|
|
49694
|
+
*
|
|
49695
|
+
* Levels outside the Anthropic `output_config` vocabulary (`none`, `minimal`)
|
|
49696
|
+
* are filtered out first: they are valid clamp *inputs* on the Responses
|
|
49697
|
+
* boundary but never valid clamp *targets*, since silently landing on `none`
|
|
49698
|
+
* would disable reasoning the caller asked for.
|
|
49699
|
+
*
|
|
49700
|
+
* Returns undefined when the model advertises nothing usable, which callers
|
|
49701
|
+
* treat as "leave the request alone".
|
|
49702
|
+
*/
|
|
49703
|
+
function clampEffortToAdvertised(effort, advertised) {
|
|
49704
|
+
const supportedEfforts = advertised?.filter(isOutputConfigEffort);
|
|
49632
49705
|
if (!supportedEfforts?.length) return;
|
|
49633
49706
|
if (supportedEfforts.includes(effort)) return effort;
|
|
49634
49707
|
return supportedEfforts.reduce((highest, current) => {
|
|
@@ -49639,6 +49712,45 @@ function normalizeOutputConfigEffort(effort, model) {
|
|
|
49639
49712
|
function hasOutputConfigFormat(payload) {
|
|
49640
49713
|
return payload?.output_config?.format != null;
|
|
49641
49714
|
}
|
|
49715
|
+
/**
|
|
49716
|
+
* Whether {@link reduceOutputFormatForNativeMessages} can produce a
|
|
49717
|
+
* native-acceptable format without dropping a caller guarantee.
|
|
49718
|
+
*
|
|
49719
|
+
* Split from the reducer so strategy selection stays a pure predicate.
|
|
49720
|
+
*/
|
|
49721
|
+
function canReduceOutputFormatForNativeMessages(payload) {
|
|
49722
|
+
const format = payload?.output_config?.format;
|
|
49723
|
+
return !format || format.strict === void 0 && format.description === void 0;
|
|
49724
|
+
}
|
|
49725
|
+
/**
|
|
49726
|
+
* Reduce `output_config.format` to the shape Copilot's native `/v1/messages`
|
|
49727
|
+
* accepts.
|
|
49728
|
+
*
|
|
49729
|
+
* Probed 2026-07-26 (`scripts/probes/messages/output-format.ts`): native
|
|
49730
|
+
* Messages serves a bare `{ type, schema }` on every model that advertises
|
|
49731
|
+
* `structured_outputs`, but rejects every optional Anthropic annotation —
|
|
49732
|
+
* `output_config.format.name: Extra inputs are not permitted`, same for
|
|
49733
|
+
* `description` and `strict`. The Anthropic schema allows all three, so a
|
|
49734
|
+
* caller can legitimately send them.
|
|
49735
|
+
*
|
|
49736
|
+
* Only `name` is dropped here, and only because it is a pure label: Anthropic
|
|
49737
|
+
* documents no effect on the reply, and the Responses translator has to invent
|
|
49738
|
+
* one when the caller omits it. `description` and `strict` both influence the
|
|
49739
|
+
* reply — one guides the model's output, the other promises the schema is
|
|
49740
|
+
* enforced — so {@link canReduceOutputFormatForNativeMessages} keeps those
|
|
49741
|
+
* requests off this path entirely rather than quietly reducing them here.
|
|
49742
|
+
*/
|
|
49743
|
+
function reduceOutputFormatForNativeMessages(payload) {
|
|
49744
|
+
const format = payload.output_config?.format;
|
|
49745
|
+
if (!format || format.name === void 0) return;
|
|
49746
|
+
payload.output_config = {
|
|
49747
|
+
...payload.output_config,
|
|
49748
|
+
format: {
|
|
49749
|
+
type: format.type,
|
|
49750
|
+
schema: format.schema
|
|
49751
|
+
}
|
|
49752
|
+
};
|
|
49753
|
+
}
|
|
49642
49754
|
function budgetTokensToEffort(budget) {
|
|
49643
49755
|
if (budget >= 24e3) return "high";
|
|
49644
49756
|
if (budget >= 8e3) return "medium";
|
|
@@ -49664,6 +49776,15 @@ function convertEnabledThinkingToAdaptive(payload, model) {
|
|
|
49664
49776
|
effort: budgetTokensToEffort(budget)
|
|
49665
49777
|
};
|
|
49666
49778
|
}
|
|
49779
|
+
/**
|
|
49780
|
+
* Normalize `output_config` for the native `/v1/messages` boundary.
|
|
49781
|
+
*
|
|
49782
|
+
* Rebuilds nothing it does not have to: the effort is clamped in place and
|
|
49783
|
+
* every other key is preserved. `output_config` is loose at ingress precisely
|
|
49784
|
+
* so newer Anthropic fields can reach a model that may accept them — dropping
|
|
49785
|
+
* them here would move the failure from a visible 400 to a silent semantic
|
|
49786
|
+
* change, which is worse.
|
|
49787
|
+
*/
|
|
49667
49788
|
function sanitizeOutputConfig(payload, model) {
|
|
49668
49789
|
if (!payload.output_config) return;
|
|
49669
49790
|
if (!modelCache.supportsOutputConfig(model)) {
|
|
@@ -49672,10 +49793,14 @@ function sanitizeOutputConfig(payload, model) {
|
|
|
49672
49793
|
}
|
|
49673
49794
|
const effort = payload.output_config.effort;
|
|
49674
49795
|
if (effort == null) {
|
|
49796
|
+
if (payload.output_config.format) {
|
|
49797
|
+
delete payload.output_config.effort;
|
|
49798
|
+
return;
|
|
49799
|
+
}
|
|
49675
49800
|
delete payload.output_config;
|
|
49676
49801
|
return;
|
|
49677
49802
|
}
|
|
49678
|
-
payload.output_config =
|
|
49803
|
+
payload.output_config.effort = normalizeOutputConfigEffort(effort, model) ?? effort;
|
|
49679
49804
|
}
|
|
49680
49805
|
function normalizeCacheControlBlock(obj) {
|
|
49681
49806
|
if (obj.cache_control && typeof obj.cache_control === "object") obj.cache_control = { type: obj.cache_control.type };
|
|
@@ -49688,11 +49813,223 @@ function sanitizeCacheControl(payload) {
|
|
|
49688
49813
|
}
|
|
49689
49814
|
if (payload.tools) for (const tool of payload.tools) normalizeCacheControlBlock(tool);
|
|
49690
49815
|
}
|
|
49816
|
+
/**
|
|
49817
|
+
* Drop `top_p` when `temperature` is also present.
|
|
49818
|
+
*
|
|
49819
|
+
* Copilot's native `/v1/messages` endpoint rejects the pair outright for
|
|
49820
|
+
* non-reasoning Claude models:
|
|
49821
|
+
*
|
|
49822
|
+
* `temperature` and `top_p` cannot both be specified for this model.
|
|
49823
|
+
* Please use only one.
|
|
49824
|
+
*
|
|
49825
|
+
* Probed 2026-07-26 (`scripts/probes/sampling-params.ts`): reproduced on
|
|
49826
|
+
* claude-sonnet-4.5 and claude-haiku-4.5; every reasoning model accepted both.
|
|
49827
|
+
* Rather than leak a 400 for a combination clients send routinely, the proxy
|
|
49828
|
+
* keeps `temperature` — the more widely used control, and the one both the
|
|
49829
|
+
* Anthropic and OpenAI defaults are expressed in — and drops `top_p`.
|
|
49830
|
+
*
|
|
49831
|
+
* Applied unconditionally on this boundary: models that accept the pair are
|
|
49832
|
+
* unaffected in practice, since sending only `temperature` is always valid.
|
|
49833
|
+
*/
|
|
49834
|
+
function sanitizeExclusiveSamplingParams(payload) {
|
|
49835
|
+
if (payload.temperature === void 0 || payload.top_p === void 0) return;
|
|
49836
|
+
consola.warn(`Dropped top_p=${payload.top_p}: Copilot rejects temperature and top_p together on /v1/messages. Keeping temperature=${payload.temperature}.`);
|
|
49837
|
+
delete payload.top_p;
|
|
49838
|
+
}
|
|
49691
49839
|
//#endregion
|
|
49692
|
-
//#region src/transform/
|
|
49693
|
-
|
|
49694
|
-
|
|
49695
|
-
|
|
49840
|
+
//#region src/transform/parameter-filter.ts
|
|
49841
|
+
/**
|
|
49842
|
+
* Parameters the default rule strips for reasoning models on the Responses
|
|
49843
|
+
* boundary. Reasoning models (gpt-5 family, o-series, codex) reject sampling
|
|
49844
|
+
* parameters upstream with a 400 "Unsupported parameter" error, so the proxy
|
|
49845
|
+
* drops them instead of leaking the incompatibility to the client.
|
|
49846
|
+
*/
|
|
49847
|
+
const DEFAULT_REASONING_UNSUPPORTED_PARAMS = ["temperature", "top_p"];
|
|
49848
|
+
/**
|
|
49849
|
+
* Models that accept a parameter the blanket reasoning rule would otherwise
|
|
49850
|
+
* strip. Probed 2026-07-26 (`scripts/probes/sampling-params.ts`): every
|
|
49851
|
+
* `/responses` reasoning model rejected `temperature`, but `gpt-5.3-codex`
|
|
49852
|
+
* accepted `top_p` (200) while its siblings returned
|
|
49853
|
+
* `Unsupported parameter: 'top_p' is not supported with this model`.
|
|
49854
|
+
*
|
|
49855
|
+
* Copilot does not advertise per-parameter support — `capabilities.supports`
|
|
49856
|
+
* is byte-identical across gpt-5.3-codex, gpt-5.4 and gpt-5.4-mini — so this
|
|
49857
|
+
* cannot be derived and has to be an evidence-backed glob list. Re-run the
|
|
49858
|
+
* probe when new models appear.
|
|
49859
|
+
*/
|
|
49860
|
+
const REASONING_PARAM_EXEMPTIONS = [{
|
|
49861
|
+
models: ["*-codex", "*-codex-*"],
|
|
49862
|
+
params: ["top_p"]
|
|
49863
|
+
}];
|
|
49864
|
+
/**
|
|
49865
|
+
* A reasoning model is any model that advertises one or more
|
|
49866
|
+
* `reasoning_effort` levels. This dynamically covers the full reasoning
|
|
49867
|
+
* family (mini, codex, future point releases) without a hardcoded ID list.
|
|
49868
|
+
*/
|
|
49869
|
+
function isReasoningModel(model) {
|
|
49870
|
+
return modelCache.supportsReasoningEffort(model);
|
|
49871
|
+
}
|
|
49872
|
+
/**
|
|
49873
|
+
* Resolve the set of request parameters to strip for a given model on the
|
|
49874
|
+
* Responses boundary.
|
|
49875
|
+
*
|
|
49876
|
+
* Rule composition:
|
|
49877
|
+
* 1. Built-in default: reasoning models strip {@link DEFAULT_REASONING_UNSUPPORTED_PARAMS}.
|
|
49878
|
+
* Disabled entirely when `responsesApiParameterFiltersReplaceDefault` is true.
|
|
49879
|
+
* 2. User rules (`responsesApiParameterFilters`): every rule whose `models`
|
|
49880
|
+
* glob matches the resolved model id contributes its `params`.
|
|
49881
|
+
*
|
|
49882
|
+
* The result is the union of all matching rules, so user rules ADD to the
|
|
49883
|
+
* default. Setting `responsesApiParameterFiltersReplaceDefault: true` disables
|
|
49884
|
+
* the default so user rules fully OVERWRITE it.
|
|
49885
|
+
*/
|
|
49886
|
+
function resolveStrippedResponsesParams(model) {
|
|
49887
|
+
const params = /* @__PURE__ */ new Set();
|
|
49888
|
+
const modelId = model?.id;
|
|
49889
|
+
if (!configStore.shouldReplaceDefaultParameterFilters() && isReasoningModel(model)) {
|
|
49890
|
+
const exempt = modelId ? resolveReasoningExemptions(modelId) : /* @__PURE__ */ new Set();
|
|
49891
|
+
for (const param of DEFAULT_REASONING_UNSUPPORTED_PARAMS) if (!exempt.has(param)) params.add(param);
|
|
49892
|
+
}
|
|
49893
|
+
if (modelId) {
|
|
49894
|
+
for (const rule of configStore.getResponsesParameterFilters()) if (rule.models.some((pattern) => matchesGlob(pattern, modelId))) for (const param of rule.params) params.add(param);
|
|
49895
|
+
}
|
|
49896
|
+
return params;
|
|
49897
|
+
}
|
|
49898
|
+
/**
|
|
49899
|
+
* Params the default reasoning rule must NOT strip for this model.
|
|
49900
|
+
*
|
|
49901
|
+
* Exemptions only narrow the built-in default — a user rule naming the same
|
|
49902
|
+
* param still strips it, so an operator can always be more conservative than
|
|
49903
|
+
* the probe evidence.
|
|
49904
|
+
*/
|
|
49905
|
+
function resolveReasoningExemptions(modelId) {
|
|
49906
|
+
const exempt = /* @__PURE__ */ new Set();
|
|
49907
|
+
for (const rule of REASONING_PARAM_EXEMPTIONS) if (rule.models.some((pattern) => matchesGlob(pattern, modelId))) for (const param of rule.params) exempt.add(param);
|
|
49908
|
+
return exempt;
|
|
49909
|
+
}
|
|
49910
|
+
/**
|
|
49911
|
+
* Strip unsupported parameters from a Responses payload before dispatch.
|
|
49912
|
+
* Keys are deleted entirely (never set to null) because upstream rejects the
|
|
49913
|
+
* mere presence of the key, not just non-null values.
|
|
49914
|
+
*/
|
|
49915
|
+
function applyResponsesParameterFilters(payload, model) {
|
|
49916
|
+
const strip = resolveStrippedResponsesParams(model);
|
|
49917
|
+
if (strip.size === 0) return;
|
|
49918
|
+
const removed = [];
|
|
49919
|
+
for (const key of strip) if (key in payload) {
|
|
49920
|
+
delete payload[key];
|
|
49921
|
+
removed.push(key);
|
|
49922
|
+
}
|
|
49923
|
+
if (removed.length > 0) consola.debug(`Stripped unsupported responses params for model ${model?.id}: ${removed.join(", ")}`);
|
|
49924
|
+
}
|
|
49925
|
+
/**
|
|
49926
|
+
* Models whose `/chat/completions` endpoint rejects `max_tokens` and require
|
|
49927
|
+
* `max_completion_tokens` instead:
|
|
49928
|
+
*
|
|
49929
|
+
* Unsupported parameter: 'max_tokens' is not supported with this model.
|
|
49930
|
+
* Use 'max_completion_tokens' instead.
|
|
49931
|
+
*
|
|
49932
|
+
* Probed 2026-07-26 (`scripts/probes/effort-and-tokens.ts`): reproduced on
|
|
49933
|
+
* gpt-5.4. Every other reachable model accepted both spellings — though for
|
|
49934
|
+
* reasoning models the two differ in meaning, since `max_tokens` counts
|
|
49935
|
+
* thinking tokens against the budget while `max_completion_tokens` does not.
|
|
49936
|
+
*
|
|
49937
|
+
* Copilot advertises nothing that distinguishes these models, so this is an
|
|
49938
|
+
* evidence-backed glob list. Extend it via `chatCompletionsUseMaxCompletionTokens`.
|
|
49939
|
+
*/
|
|
49940
|
+
const DEFAULT_MAX_COMPLETION_TOKENS_MODELS = ["gpt-5.4", "gpt-5.4-*"];
|
|
49941
|
+
/**
|
|
49942
|
+
* Rename `max_tokens` to `max_completion_tokens` for models that reject the
|
|
49943
|
+
* former. No-op when the caller sent neither, or when the model accepts
|
|
49944
|
+
* `max_tokens` — which is still the majority.
|
|
49945
|
+
*/
|
|
49946
|
+
function applyChatCompletionsTokenParam(payload, model) {
|
|
49947
|
+
if (payload.max_tokens == null) return;
|
|
49948
|
+
const modelId = model?.id;
|
|
49949
|
+
if (!modelId || !requiresMaxCompletionTokens(modelId)) return;
|
|
49950
|
+
const record = payload;
|
|
49951
|
+
record.max_completion_tokens = payload.max_tokens;
|
|
49952
|
+
delete record.max_tokens;
|
|
49953
|
+
consola.debug(`Renamed max_tokens to max_completion_tokens for model ${modelId}`);
|
|
49954
|
+
}
|
|
49955
|
+
function requiresMaxCompletionTokens(modelId) {
|
|
49956
|
+
return [...DEFAULT_MAX_COMPLETION_TOKENS_MODELS, ...configStore.getChatCompletionsMaxCompletionTokensModels()].some((pattern) => matchesGlob(pattern, modelId));
|
|
49957
|
+
}
|
|
49958
|
+
/**
|
|
49959
|
+
* Copilot's `/responses` minimum for `max_output_tokens`:
|
|
49960
|
+
*
|
|
49961
|
+
* Invalid 'max_output_tokens': integer below minimum value.
|
|
49962
|
+
* Expected a value >= 16
|
|
49963
|
+
*
|
|
49964
|
+
* Probed 2026-07-26 (`scripts/probes/effort-and-tokens.ts`): identical across
|
|
49965
|
+
* all 9 `/responses` models. The ceiling is NOT enforced — the advertised
|
|
49966
|
+
* `limits.max_output_tokens` + 1 was accepted everywhere — so only the floor
|
|
49967
|
+
* is clamped.
|
|
49968
|
+
*/
|
|
49969
|
+
const RESPONSES_MIN_OUTPUT_TOKENS = 16;
|
|
49970
|
+
/**
|
|
49971
|
+
* Raise a below-minimum `max_output_tokens` to Copilot's floor.
|
|
49972
|
+
*
|
|
49973
|
+
* The client-facing schema still accepts 0..15: those are valid OpenAI input,
|
|
49974
|
+
* and this floor is a Copilot quirk the proxy absorbs rather than leaks back
|
|
49975
|
+
* as a 400.
|
|
49976
|
+
*/
|
|
49977
|
+
function clampResponsesOutputTokens(payload) {
|
|
49978
|
+
const requested = payload.max_output_tokens;
|
|
49979
|
+
if (requested == null || requested >= RESPONSES_MIN_OUTPUT_TOKENS) return;
|
|
49980
|
+
payload.max_output_tokens = RESPONSES_MIN_OUTPUT_TOKENS;
|
|
49981
|
+
consola.debug(`Raised max_output_tokens from ${requested} to the Copilot minimum of ${RESPONSES_MIN_OUTPUT_TOKENS}`);
|
|
49982
|
+
}
|
|
49983
|
+
/**
|
|
49984
|
+
* Clamp `reasoning.effort` on the OpenAI-facing `/responses` route to a level
|
|
49985
|
+
* the resolved model advertises.
|
|
49986
|
+
*
|
|
49987
|
+
* This route forwards the caller's own vocabulary, so leaving effort alone is
|
|
49988
|
+
* defensible in isolation — but it is not a passthrough in practice. The same
|
|
49989
|
+
* `afterTransform` already strips `temperature`/`top_p` for reasoning models and
|
|
49990
|
+
* raises a below-minimum `max_output_tokens`, so effort was the one parameter
|
|
49991
|
+
* where the proxy knew the request would 400 and forwarded it anyway.
|
|
49992
|
+
*
|
|
49993
|
+
* `none` and `minimal` pass through unranked, matching the Anthropic-to-Responses
|
|
49994
|
+
* path: they are Responses-only levels rather than rungs on the effort ladder,
|
|
49995
|
+
* and clamping `none` upward would invert the caller's intent.
|
|
49996
|
+
*/
|
|
49997
|
+
function clampResponsesReasoningEffort(payload, model) {
|
|
49998
|
+
const requested = payload.reasoning?.effort;
|
|
49999
|
+
if (!requested || requested === "none" || requested === "minimal") return;
|
|
50000
|
+
const clamped = clampEffortToAdvertised(requested, model?.capabilities.supports.reasoning_effort);
|
|
50001
|
+
if (!clamped || clamped === requested) return;
|
|
50002
|
+
payload.reasoning = {
|
|
50003
|
+
...payload.reasoning,
|
|
50004
|
+
effort: clamped
|
|
50005
|
+
};
|
|
50006
|
+
consola.warn(`Lowered reasoning.effort from ${requested} to ${clamped}, the highest level ${model?.id} advertises.`);
|
|
50007
|
+
}
|
|
50008
|
+
/**
|
|
50009
|
+
* Lower a `max_tokens` above the model's advertised ceiling to that ceiling.
|
|
50010
|
+
*
|
|
50011
|
+
* Copilot's native `/v1/messages` enforces its ceiling, unlike `/responses`
|
|
50012
|
+
* where the advertised value is advisory:
|
|
50013
|
+
*
|
|
50014
|
+
* max_tokens: 64001 > 64000, which is the maximum allowed number of output
|
|
50015
|
+
* tokens for claude
|
|
50016
|
+
*
|
|
50017
|
+
* Probed 2026-07-26 (`scripts/probes/effort-and-tokens.ts`). The model record
|
|
50018
|
+
* already carries the bound, so leaking a 400 for a value the proxy could have
|
|
50019
|
+
* corrected is a worse outcome than serving a shorter completion.
|
|
50020
|
+
*
|
|
50021
|
+
* Unlike the `/responses` floor this is a real semantic change — the caller
|
|
50022
|
+
* receives less output than they asked for — so it warns rather than logs at
|
|
50023
|
+
* debug level. No advertised ceiling means no clamp: an unknown bound is not a
|
|
50024
|
+
* reason to guess one.
|
|
50025
|
+
*/
|
|
50026
|
+
function clampMessagesOutputTokens(payload, model) {
|
|
50027
|
+
const ceiling = model?.capabilities.limits.max_output_tokens;
|
|
50028
|
+
const requested = payload.max_tokens;
|
|
50029
|
+
if (ceiling == null || requested == null || requested <= ceiling) return;
|
|
50030
|
+
payload.max_tokens = ceiling;
|
|
50031
|
+
consola.warn(`Lowered max_tokens from ${requested} to ${ceiling}, the ceiling ${model?.id} advertises. Copilot rejects a higher value outright on /v1/messages.`);
|
|
50032
|
+
}
|
|
49696
50033
|
//#endregion
|
|
49697
50034
|
//#region src/translator/anthropic/document.ts
|
|
49698
50035
|
/** Trimmed text of a `content`-source document part, or undefined when it carries none. */
|
|
@@ -49890,6 +50227,7 @@ function normalizeAnthropicRequest(payload) {
|
|
|
49890
50227
|
})),
|
|
49891
50228
|
toolChoice: normalizeToolChoice(payload.tool_choice),
|
|
49892
50229
|
thinking: normalizeThinking(payload.thinking),
|
|
50230
|
+
outputEffort: payload.output_config?.effort ?? void 0,
|
|
49893
50231
|
serviceTier: payload.service_tier
|
|
49894
50232
|
};
|
|
49895
50233
|
}
|
|
@@ -50364,11 +50702,6 @@ function toConversationTurn(turn) {
|
|
|
50364
50702
|
};
|
|
50365
50703
|
}
|
|
50366
50704
|
function recordAnthropicRequestIssues(request, context) {
|
|
50367
|
-
if (request.topK !== void 0) context.record({
|
|
50368
|
-
kind: "unsupported_top_k",
|
|
50369
|
-
severity: "warning",
|
|
50370
|
-
message: "Anthropic top_k is not supported by the upstream Copilot CAPI payload and was dropped."
|
|
50371
|
-
}, { fatalInStrict: true });
|
|
50372
50705
|
if (request.serviceTier !== void 0) context.record({
|
|
50373
50706
|
kind: "unsupported_service_tier",
|
|
50374
50707
|
severity: "warning",
|
|
@@ -50435,6 +50768,7 @@ function normalizeAnthropicConversation(payload, policy) {
|
|
|
50435
50768
|
stream: normalized.stream,
|
|
50436
50769
|
temperature: normalized.temperature,
|
|
50437
50770
|
topP: normalized.topP,
|
|
50771
|
+
topK: normalized.topK,
|
|
50438
50772
|
userId: normalized.userId,
|
|
50439
50773
|
tools: normalized.tools?.map((tool) => ({
|
|
50440
50774
|
name: tool.name,
|
|
@@ -50442,7 +50776,8 @@ function normalizeAnthropicConversation(payload, policy) {
|
|
|
50442
50776
|
inputSchema: tool.inputSchema
|
|
50443
50777
|
})),
|
|
50444
50778
|
toolChoice: normalized.toolChoice,
|
|
50445
|
-
thinking: normalized.thinking
|
|
50779
|
+
thinking: normalized.thinking,
|
|
50780
|
+
outputEffort: normalized.outputEffort ?? void 0
|
|
50446
50781
|
},
|
|
50447
50782
|
issues: context.getIssues()
|
|
50448
50783
|
};
|
|
@@ -50764,7 +51099,6 @@ async function handleCompletionCore({ body, signal, headers }) {
|
|
|
50764
51099
|
headers
|
|
50765
51100
|
}, {
|
|
50766
51101
|
protocol: "openai-chat",
|
|
50767
|
-
transformChain: chatCompletionsModelChain,
|
|
50768
51102
|
strategyRegistry: chatCompletionsStrategyRegistry,
|
|
50769
51103
|
afterIngest({ payload }) {
|
|
50770
51104
|
consola.debug("Request payload:", JSON.stringify(payload).slice(-400));
|
|
@@ -50783,6 +51117,7 @@ async function handleCompletionCore({ body, signal, headers }) {
|
|
|
50783
51117
|
payload.max_tokens = selectedModel?.capabilities.limits.max_output_tokens;
|
|
50784
51118
|
consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
|
|
50785
51119
|
}
|
|
51120
|
+
applyChatCompletionsTokenParam(payload, selectedModel);
|
|
50786
51121
|
},
|
|
50787
51122
|
buildStrategyContext({ payload, meta, copilotClient, upstreamSignal, modelMapping }) {
|
|
50788
51123
|
return {
|
|
@@ -50820,16 +51155,27 @@ function normalizeEmbeddingRequest(payload) {
|
|
|
50820
51155
|
}
|
|
50821
51156
|
/**
|
|
50822
51157
|
* Core handler for creating embeddings.
|
|
51158
|
+
*
|
|
51159
|
+
* `client` is an injection seam for tests; production callers omit it. This
|
|
51160
|
+
* route does not go through `runPipeline`, so it constructs its own client and
|
|
51161
|
+
* derives its own upstream signal — otherwise a client disconnect would leave
|
|
51162
|
+
* the upstream request running and the configured timeout unenforced.
|
|
50823
51163
|
*/
|
|
50824
|
-
async function handleEmbeddingsCore(body, headers) {
|
|
51164
|
+
async function handleEmbeddingsCore(body, headers, client, signal) {
|
|
50825
51165
|
const { payload } = protocolRegistry.ingest("embeddings", body, headers);
|
|
50826
|
-
|
|
51166
|
+
const copilotClient = client ?? createCopilotClient();
|
|
51167
|
+
const upstreamSignal = signal ? createUpstreamSignalFromConfig(signal) : void 0;
|
|
51168
|
+
try {
|
|
51169
|
+
return await copilotClient.createEmbeddings(normalizeEmbeddingRequest(payload), { signal: upstreamSignal?.signal });
|
|
51170
|
+
} finally {
|
|
51171
|
+
upstreamSignal?.cleanup();
|
|
51172
|
+
}
|
|
50827
51173
|
}
|
|
50828
51174
|
//#endregion
|
|
50829
51175
|
//#region src/routes/embeddings/route.ts
|
|
50830
51176
|
function createEmbeddingRoutes() {
|
|
50831
51177
|
return new Elysia().use(requestGuardPlugin).post("/embeddings", async ({ body, request }) => {
|
|
50832
|
-
return handleEmbeddingsCore(body, request.headers);
|
|
51178
|
+
return handleEmbeddingsCore(body, request.headers, void 0, request.signal);
|
|
50833
51179
|
}, { guarded: true });
|
|
50834
51180
|
}
|
|
50835
51181
|
//#endregion
|
|
@@ -50883,6 +51229,20 @@ async function handleCountTokensCore({ body, headers }) {
|
|
|
50883
51229
|
return { input_tokens: finalTokenCount };
|
|
50884
51230
|
}
|
|
50885
51231
|
//#endregion
|
|
51232
|
+
//#region src/transform/beta-headers.ts
|
|
51233
|
+
const COPILOT_UNSUPPORTED_BETA_RE = /^mid-conversation-system-\d{4}-\d{2}-\d{2}$/;
|
|
51234
|
+
function processAnthropicBetaHeader(rawHeader) {
|
|
51235
|
+
if (!rawHeader) return void 0;
|
|
51236
|
+
const values = rawHeader.split(",").map((v) => v.trim()).filter(Boolean);
|
|
51237
|
+
const filtered = [];
|
|
51238
|
+
for (const value of values) {
|
|
51239
|
+
if (CONTEXT_BETA_RE.test(value)) continue;
|
|
51240
|
+
if (COPILOT_UNSUPPORTED_BETA_RE.test(value)) continue;
|
|
51241
|
+
filtered.push(value);
|
|
51242
|
+
}
|
|
51243
|
+
return filtered.length > 0 ? filtered.join(",") : void 0;
|
|
51244
|
+
}
|
|
51245
|
+
//#endregion
|
|
50886
51246
|
//#region src/transform/context-management.ts
|
|
50887
51247
|
/** Default token threshold when model limits are unknown. */
|
|
50888
51248
|
const DEFAULT_COMPACT_THRESHOLD = 5e4;
|
|
@@ -50944,59 +51304,32 @@ function containsVisionContent(value) {
|
|
|
50944
51304
|
return false;
|
|
50945
51305
|
}
|
|
50946
51306
|
//#endregion
|
|
50947
|
-
//#region src/transform/
|
|
51307
|
+
//#region src/transform/responses-input.ts
|
|
50948
51308
|
/**
|
|
50949
|
-
*
|
|
50950
|
-
* boundary. Reasoning models (gpt-5 family, o-series, codex) reject sampling
|
|
50951
|
-
* parameters upstream with a 400 "Unsupported parameter" error, so the proxy
|
|
50952
|
-
* drops them instead of leaking the incompatibility to the client.
|
|
50953
|
-
*/
|
|
50954
|
-
const DEFAULT_REASONING_UNSUPPORTED_PARAMS = ["temperature", "top_p"];
|
|
50955
|
-
/**
|
|
50956
|
-
* A reasoning model is any model that advertises one or more
|
|
50957
|
-
* `reasoning_effort` levels. This dynamically covers the full reasoning
|
|
50958
|
-
* family (mini, codex, future point releases) without a hardcoded ID list.
|
|
50959
|
-
*/
|
|
50960
|
-
function isReasoningModel(model) {
|
|
50961
|
-
return modelCache.supportsReasoningEffort(model);
|
|
50962
|
-
}
|
|
50963
|
-
/**
|
|
50964
|
-
* Resolve the set of request parameters to strip for a given model on the
|
|
50965
|
-
* Responses boundary.
|
|
51309
|
+
* Strip `phase` from input message items.
|
|
50966
51310
|
*
|
|
50967
|
-
*
|
|
50968
|
-
*
|
|
50969
|
-
*
|
|
50970
|
-
*
|
|
50971
|
-
* glob matches the resolved model id contributes its `params`.
|
|
51311
|
+
* `phase` (`commentary` / `final_answer`) is an output-only annotation. Some
|
|
51312
|
+
* models reject it when it is sent back as input — this repo traced an
|
|
51313
|
+
* upstream `400 invalid_request_body` to exactly that (see
|
|
51314
|
+
* `docs/investigation-responses-404.md`).
|
|
50972
51315
|
*
|
|
50973
|
-
*
|
|
50974
|
-
*
|
|
50975
|
-
* the
|
|
51316
|
+
* Both Responses dispatch paths must call this. `POST /responses` receives the
|
|
51317
|
+
* field from clients replaying prior output; the `/v1/messages` → Responses
|
|
51318
|
+
* strategy has the translator *generate* it (see `resolveAssistantPhase` in
|
|
51319
|
+
* `translator/responses/response-items.ts`), so neither path is exempt.
|
|
50976
51320
|
*/
|
|
50977
|
-
function
|
|
50978
|
-
|
|
50979
|
-
|
|
50980
|
-
const
|
|
50981
|
-
|
|
50982
|
-
|
|
50983
|
-
|
|
50984
|
-
|
|
50985
|
-
|
|
50986
|
-
|
|
50987
|
-
* Strip unsupported parameters from a Responses payload before dispatch.
|
|
50988
|
-
* Keys are deleted entirely (never set to null) because upstream rejects the
|
|
50989
|
-
* mere presence of the key, not just non-null values.
|
|
50990
|
-
*/
|
|
50991
|
-
function applyResponsesParameterFilters(payload, model) {
|
|
50992
|
-
const strip = resolveStrippedResponsesParams(model);
|
|
50993
|
-
if (strip.size === 0) return;
|
|
50994
|
-
const removed = [];
|
|
50995
|
-
for (const key of strip) if (key in payload) {
|
|
50996
|
-
delete payload[key];
|
|
50997
|
-
removed.push(key);
|
|
51321
|
+
function stripPhaseFromInputMessages(payload) {
|
|
51322
|
+
if (!Array.isArray(payload.input)) return;
|
|
51323
|
+
let stripped = 0;
|
|
51324
|
+
for (const item of payload.input) {
|
|
51325
|
+
if (typeof item !== "object" || item === null) continue;
|
|
51326
|
+
const rec = item;
|
|
51327
|
+
if ((!("type" in rec) || rec.type === "message") && "phase" in rec) {
|
|
51328
|
+
delete rec.phase;
|
|
51329
|
+
stripped++;
|
|
51330
|
+
}
|
|
50998
51331
|
}
|
|
50999
|
-
if (
|
|
51332
|
+
if (stripped > 0) consola.debug(`Stripped phase from ${stripped} input message item(s)`);
|
|
51000
51333
|
}
|
|
51001
51334
|
//#endregion
|
|
51002
51335
|
//#region src/translator/responses/function-schema.ts
|
|
@@ -51218,6 +51551,7 @@ function translateAnthropicToResponsesPayload(payload, options) {
|
|
|
51218
51551
|
instructions: translateSystemPrompt(payload.system),
|
|
51219
51552
|
temperature: payload.temperature ?? null,
|
|
51220
51553
|
top_p: payload.top_p ?? null,
|
|
51554
|
+
...payload.top_k !== void 0 ? { top_k: payload.top_k } : {},
|
|
51221
51555
|
max_output_tokens: payload.max_tokens,
|
|
51222
51556
|
tools: convertAnthropicTools(payload.tools),
|
|
51223
51557
|
tool_choice: convertAnthropicToolChoice(payload.tool_choice),
|
|
@@ -51387,25 +51721,49 @@ function resolveResponsesTextConfig(payload) {
|
|
|
51387
51721
|
} };
|
|
51388
51722
|
}
|
|
51389
51723
|
}
|
|
51724
|
+
/**
|
|
51725
|
+
* Resolve the Responses `reasoning.effort` for an Anthropic request.
|
|
51726
|
+
*
|
|
51727
|
+
* Every branch below produces a *candidate* the caller did not name directly —
|
|
51728
|
+
* a hardcoded tier, a config default, or a mapped `output_config.effort` — so
|
|
51729
|
+
* all of them are clamped at the single exit. Clamping only the
|
|
51730
|
+
* `output_config.effort` branch left the others able to emit a level the model
|
|
51731
|
+
* rejects: `adaptive` sent `medium` to a model advertising `[high, xhigh, max]`,
|
|
51732
|
+
* and `enabled` sent the configured default with an `as` cast and no check.
|
|
51733
|
+
*/
|
|
51390
51734
|
function resolveResponsesReasoningEffort(payload, options) {
|
|
51735
|
+
const candidate = resolveEffortCandidate(payload, options);
|
|
51736
|
+
if (!candidate) return candidate;
|
|
51737
|
+
return clampResponsesEffort(candidate, options);
|
|
51738
|
+
}
|
|
51739
|
+
function resolveEffortCandidate(payload, options) {
|
|
51391
51740
|
if (payload.thinking?.type === "disabled") return "none";
|
|
51392
|
-
if (payload.output_config?.effort) return
|
|
51741
|
+
if (payload.output_config?.effort) return payload.output_config.effort;
|
|
51393
51742
|
if (payload.thinking?.type === "adaptive") return "medium";
|
|
51394
51743
|
if (payload.thinking?.type === "enabled") return options?.reasoningEffortResolver?.(payload.model) ?? "medium";
|
|
51395
51744
|
}
|
|
51396
|
-
|
|
51397
|
-
|
|
51398
|
-
|
|
51745
|
+
/**
|
|
51746
|
+
* Clamp a Responses effort to what the target model advertises.
|
|
51747
|
+
*
|
|
51748
|
+
* `none` and `minimal` belong to the Responses vocabulary but not to the
|
|
51749
|
+
* Anthropic `output_config` ladder, so they are passed through rather than
|
|
51750
|
+
* ranked: `none` means "do not reason", and clamping it *up* to the model's
|
|
51751
|
+
* highest advertised level would invert the caller's intent. Every model
|
|
51752
|
+
* observed on `/responses` advertises `none` (probed 2026-07-26).
|
|
51753
|
+
*
|
|
51754
|
+
* With no advertised list the effort passes through untouched — with nothing to
|
|
51755
|
+
* derive from, forwarding the request beats guessing at a level that may be both
|
|
51756
|
+
* a downgrade and still unsupported.
|
|
51757
|
+
*/
|
|
51758
|
+
function clampResponsesEffort(effort, options) {
|
|
51759
|
+
if (effort === "none" || effort === "minimal") return effort;
|
|
51760
|
+
return clampEffortToAdvertised(effort, options?.supportedEfforts) ?? effort;
|
|
51399
51761
|
}
|
|
51400
51762
|
function assertResponsesCompatibleRequest(payload) {
|
|
51401
51763
|
if (payload.stop_sequences?.length) throw new TranslationFailure("Anthropic stop_sequences cannot be forwarded through the Responses execution path.", {
|
|
51402
51764
|
status: 400,
|
|
51403
51765
|
kind: "unsupported_stop_sequences"
|
|
51404
51766
|
});
|
|
51405
|
-
if (payload.top_k !== void 0) throw new TranslationFailure("Anthropic top_k is not supported on the Responses execution path.", {
|
|
51406
|
-
status: 400,
|
|
51407
|
-
kind: "unsupported_top_k"
|
|
51408
|
-
});
|
|
51409
51767
|
if (payload.service_tier !== void 0) throw new TranslationFailure("Anthropic service_tier is not supported on the Responses execution path.", {
|
|
51410
51768
|
status: 400,
|
|
51411
51769
|
kind: "unsupported_service_tier"
|
|
@@ -51687,10 +52045,12 @@ function mapResponsesUsage(response) {
|
|
|
51687
52045
|
const inputTokens = response.usage?.input_tokens ?? 0;
|
|
51688
52046
|
const outputTokens = response.usage?.output_tokens ?? 0;
|
|
51689
52047
|
const cachedTokens = response.usage?.input_tokens_details?.cached_tokens;
|
|
52048
|
+
const writtenTokens = response.usage?.input_tokens_details?.cache_write_tokens;
|
|
51690
52049
|
return {
|
|
51691
52050
|
input_tokens: inputTokens - (cachedTokens ?? 0),
|
|
51692
52051
|
output_tokens: outputTokens,
|
|
51693
|
-
...cachedTokens !== void 0 ? { cache_read_input_tokens: cachedTokens } : {}
|
|
52052
|
+
...cachedTokens !== void 0 ? { cache_read_input_tokens: cachedTokens } : {},
|
|
52053
|
+
...writtenTokens ? { cache_creation_input_tokens: writtenTokens } : {}
|
|
51694
52054
|
};
|
|
51695
52055
|
}
|
|
51696
52056
|
function isRecord$1(value) {
|
|
@@ -52143,12 +52503,15 @@ function createMessagesViaResponsesStrategy(copilotClient, responsesPayload, opt
|
|
|
52143
52503
|
//#region src/routes/messages/strategy-registry.ts
|
|
52144
52504
|
const nativeMessagesEntry = {
|
|
52145
52505
|
name: "native-messages",
|
|
52146
|
-
canHandle: (model, ctx) => modelCache.supportsEndpoint(model, "/v1/messages") && !hasOutputConfigFormat(ctx?.anthropicPayload),
|
|
52506
|
+
canHandle: (model, ctx) => modelCache.supportsEndpoint(model, "/v1/messages") && (!hasOutputConfigFormat(ctx?.anthropicPayload) || modelCache.supportsStructuredOutputs(model) && canReduceOutputFormatForNativeMessages(ctx?.anthropicPayload)),
|
|
52147
52507
|
async execute(ctx) {
|
|
52148
52508
|
convertEnabledThinkingToAdaptive(ctx.anthropicPayload, ctx.selectedModel);
|
|
52149
52509
|
filterThinkingBlocksForNativeMessages(ctx.anthropicPayload);
|
|
52150
52510
|
sanitizeOutputConfig(ctx.anthropicPayload, ctx.selectedModel);
|
|
52511
|
+
reduceOutputFormatForNativeMessages(ctx.anthropicPayload);
|
|
52512
|
+
sanitizeExclusiveSamplingParams(ctx.anthropicPayload);
|
|
52151
52513
|
sanitizeCacheControl(ctx.anthropicPayload);
|
|
52514
|
+
clampMessagesOutputTokens(ctx.anthropicPayload, ctx.selectedModel);
|
|
52152
52515
|
return await runStrategy(createNativeMessagesStrategy(ctx.copilotClient, ctx.anthropicPayload, ctx.anthropicBetaHeader, {
|
|
52153
52516
|
signal: ctx.upstreamSignal.signal,
|
|
52154
52517
|
requestContext: ctx.requestContext
|
|
@@ -52159,10 +52522,15 @@ const responsesApiEntry = {
|
|
|
52159
52522
|
name: "responses-api",
|
|
52160
52523
|
canHandle: (model) => modelCache.supportsEndpoint(model, RESPONSES_ENDPOINT),
|
|
52161
52524
|
async execute(ctx) {
|
|
52162
|
-
const responsesPayload = withTranslationErrors(() => translateAnthropicToResponsesPayload(ctx.anthropicPayload, {
|
|
52525
|
+
const responsesPayload = withTranslationErrors(() => translateAnthropicToResponsesPayload(ctx.anthropicPayload, {
|
|
52526
|
+
reasoningEffortResolver: (model) => configStore.getReasoningEffort(model),
|
|
52527
|
+
supportedEfforts: ctx.selectedModel?.capabilities.supports.reasoning_effort
|
|
52528
|
+
}));
|
|
52163
52529
|
applyContextManagement(responsesPayload, ctx.selectedModel?.capabilities.limits.max_prompt_tokens);
|
|
52164
52530
|
compactInputByLatestCompaction(responsesPayload);
|
|
52531
|
+
stripPhaseFromInputMessages(responsesPayload);
|
|
52165
52532
|
applyResponsesParameterFilters(responsesPayload, ctx.selectedModel);
|
|
52533
|
+
clampResponsesOutputTokens(responsesPayload);
|
|
52166
52534
|
const { vision, initiator } = getResponsesRequestOptions(responsesPayload);
|
|
52167
52535
|
return await runStrategy(createMessagesViaResponsesStrategy(ctx.copilotClient, responsesPayload, {
|
|
52168
52536
|
vision,
|
|
@@ -52199,7 +52567,7 @@ async function handleMessagesCore({ body, signal, headers }) {
|
|
|
52199
52567
|
headers
|
|
52200
52568
|
}, {
|
|
52201
52569
|
protocol: "anthropic-messages",
|
|
52202
|
-
|
|
52570
|
+
applyModelPolicy: true,
|
|
52203
52571
|
strategyRegistry: defaultStrategyRegistry,
|
|
52204
52572
|
afterIngest({ payload, headers: reqHeaders }) {
|
|
52205
52573
|
if (consola.level >= 4) consola.debug("Anthropic request payload:", JSON.stringify(payload));
|
|
@@ -52244,9 +52612,13 @@ function createMessageRoutes() {
|
|
|
52244
52612
|
//#region src/routes/models/handler.ts
|
|
52245
52613
|
/**
|
|
52246
52614
|
* Core handler for listing models.
|
|
52615
|
+
*
|
|
52616
|
+
* `client` is an injection seam for tests; production callers omit it. This
|
|
52617
|
+
* route does not go through `runPipeline`, so it constructs its own client on
|
|
52618
|
+
* a cache miss.
|
|
52247
52619
|
*/
|
|
52248
|
-
async function handleModelsCore() {
|
|
52249
|
-
if (!modelCache.getModels()) await cacheModels(createCopilotClient());
|
|
52620
|
+
async function handleModelsCore(client) {
|
|
52621
|
+
if (!modelCache.getModels()) await cacheModels(client ?? createCopilotClient());
|
|
52250
52622
|
return {
|
|
52251
52623
|
object: "list",
|
|
52252
52624
|
data: modelCache.getModels()?.data.map((model) => ({
|
|
@@ -52264,9 +52636,9 @@ async function handleModelsCore() {
|
|
|
52264
52636
|
//#endregion
|
|
52265
52637
|
//#region src/routes/models/route.ts
|
|
52266
52638
|
function createModelRoutes() {
|
|
52267
|
-
return new Elysia().get("/models", async () => {
|
|
52639
|
+
return new Elysia().use(requestGuardPlugin).get("/models", async () => {
|
|
52268
52640
|
return handleModelsCore();
|
|
52269
|
-
});
|
|
52641
|
+
}, { guarded: true });
|
|
52270
52642
|
}
|
|
52271
52643
|
//#endregion
|
|
52272
52644
|
//#region src/routes/responses/emulator.ts
|
|
@@ -52646,7 +53018,6 @@ async function handleResponsesCore({ body, signal, headers }) {
|
|
|
52646
53018
|
headers
|
|
52647
53019
|
}, {
|
|
52648
53020
|
protocol: "responses",
|
|
52649
|
-
transformChain: responsesModelChain,
|
|
52650
53021
|
strategyRegistry: responsesStrategyRegistry,
|
|
52651
53022
|
afterIngest({ payload }) {
|
|
52652
53023
|
originalPayload = payload;
|
|
@@ -52661,6 +53032,8 @@ async function handleResponsesCore({ body, signal, headers }) {
|
|
|
52661
53032
|
if (!modelCache.supportsEndpoint(selectedModel, "/responses")) throwInvalidRequestError("The selected model does not support the responses endpoint.", "model");
|
|
52662
53033
|
applyContextManagement(payload, selectedModel.capabilities.limits.max_prompt_tokens);
|
|
52663
53034
|
applyResponsesParameterFilters(payload, selectedModel);
|
|
53035
|
+
clampResponsesOutputTokens(payload);
|
|
53036
|
+
clampResponsesReasoningEffort(payload, selectedModel);
|
|
52664
53037
|
},
|
|
52665
53038
|
buildStrategyContext({ payload, meta, copilotClient, upstreamSignal }) {
|
|
52666
53039
|
const { vision, initiator } = getResponsesRequestOptions(payload);
|
|
@@ -52733,23 +53106,6 @@ function applyResponsesInputPolicies(payload) {
|
|
|
52733
53106
|
rejectUnsupportedRemoteImageUrls(payload);
|
|
52734
53107
|
}
|
|
52735
53108
|
/**
|
|
52736
|
-
* Strip `phase` from input message items. The `phase` field is an output
|
|
52737
|
-
* annotation that some models may reject when sent back as input.
|
|
52738
|
-
*/
|
|
52739
|
-
function stripPhaseFromInputMessages(payload) {
|
|
52740
|
-
if (!Array.isArray(payload.input)) return;
|
|
52741
|
-
let stripped = 0;
|
|
52742
|
-
for (const item of payload.input) {
|
|
52743
|
-
if (typeof item !== "object" || item === null) continue;
|
|
52744
|
-
const rec = item;
|
|
52745
|
-
if ((!("type" in rec) || rec.type === "message") && "phase" in rec) {
|
|
52746
|
-
delete rec.phase;
|
|
52747
|
-
stripped++;
|
|
52748
|
-
}
|
|
52749
|
-
}
|
|
52750
|
-
if (stripped > 0) consola.debug(`Stripped phase from ${stripped} input message item(s)`);
|
|
52751
|
-
}
|
|
52752
|
-
/**
|
|
52753
53109
|
* Remove input items that Copilot cannot resolve and would trigger 404:
|
|
52754
53110
|
* - `item_reference` items (opaque IDs from store=true sessions)
|
|
52755
53111
|
* - `function_call_output` items whose `call_id` has no matching prior
|
|
@@ -52822,35 +53178,42 @@ var UpstreamResourceDispatcher = class {
|
|
|
52822
53178
|
return this.client.deleteResponse(responseId, options);
|
|
52823
53179
|
}
|
|
52824
53180
|
};
|
|
52825
|
-
|
|
52826
|
-
|
|
53181
|
+
/**
|
|
53182
|
+
* Build the dispatcher backing the `/responses/{id}` resource routes.
|
|
53183
|
+
*
|
|
53184
|
+
* `client` is an injection seam for tests: pass a stand-in to exercise the
|
|
53185
|
+
* upstream path without patching `CopilotClient.prototype`. Production callers
|
|
53186
|
+
* omit it and get the configured client.
|
|
53187
|
+
*/
|
|
53188
|
+
function createResourceDispatcher(client) {
|
|
53189
|
+
return configStore.isEmulatorEnabled() ? new EmulatorResourceDispatcher() : new UpstreamResourceDispatcher(client ?? createCopilotClient());
|
|
52827
53190
|
}
|
|
52828
53191
|
//#endregion
|
|
52829
53192
|
//#region src/routes/responses/resource-handler.ts
|
|
52830
|
-
async function handleRetrieveResponseCore({ params, url, headers, signal }) {
|
|
53193
|
+
async function handleRetrieveResponseCore({ params, url, headers, signal, client }) {
|
|
52831
53194
|
const responseId = requireResponseId(params.responseId);
|
|
52832
|
-
return await createResourceDispatcher().retrieve(responseId, getRetrieveParamsFromUrl(url), {
|
|
53195
|
+
return await createResourceDispatcher(client).retrieve(responseId, getRetrieveParamsFromUrl(url), {
|
|
52833
53196
|
requestContext: readCapiRequestContext(headers),
|
|
52834
53197
|
signal
|
|
52835
53198
|
});
|
|
52836
53199
|
}
|
|
52837
|
-
async function handleListResponseInputItemsCore({ params, url, headers, signal }) {
|
|
53200
|
+
async function handleListResponseInputItemsCore({ params, url, headers, signal, client }) {
|
|
52838
53201
|
const responseId = requireResponseId(params.responseId);
|
|
52839
|
-
return await createResourceDispatcher().listInputItems(responseId, getInputItemsParamsFromUrl(url), {
|
|
53202
|
+
return await createResourceDispatcher(client).listInputItems(responseId, getInputItemsParamsFromUrl(url), {
|
|
52840
53203
|
requestContext: readCapiRequestContext(headers),
|
|
52841
53204
|
signal
|
|
52842
53205
|
});
|
|
52843
53206
|
}
|
|
52844
|
-
async function handleCreateResponseInputTokensCore({ body, headers, signal }) {
|
|
53207
|
+
async function handleCreateResponseInputTokensCore({ body, headers, signal, client }) {
|
|
52845
53208
|
const { payload, meta } = protocolRegistry.ingest("responses-input-tokens", body, headers);
|
|
52846
|
-
return await createResourceDispatcher().createInputTokens(payload, {
|
|
53209
|
+
return await createResourceDispatcher(client).createInputTokens(payload, {
|
|
52847
53210
|
requestContext: meta.requestContext,
|
|
52848
53211
|
signal
|
|
52849
53212
|
});
|
|
52850
53213
|
}
|
|
52851
|
-
async function handleDeleteResponseCore({ params, headers, signal }) {
|
|
53214
|
+
async function handleDeleteResponseCore({ params, headers, signal, client }) {
|
|
52852
53215
|
const responseId = requireResponseId(params.responseId);
|
|
52853
|
-
return await createResourceDispatcher().delete(responseId, {
|
|
53216
|
+
return await createResourceDispatcher(client).delete(responseId, {
|
|
52854
53217
|
requestContext: readCapiRequestContext(headers),
|
|
52855
53218
|
signal
|
|
52856
53219
|
});
|
|
@@ -53077,9 +53440,7 @@ async function runServer(options) {
|
|
|
53077
53440
|
baseDelayMs: secondsToMs(upstreamQueueBaseDelaySeconds),
|
|
53078
53441
|
maxDelayMs: secondsToMs(upstreamQueueMaxDelaySeconds)
|
|
53079
53442
|
});
|
|
53080
|
-
authStore.gheDomain
|
|
53081
|
-
if (options.gheDomain !== void 0) authStore.gheDomain = options.gheDomain ? normalizeGheDomain(options.gheDomain) : void 0;
|
|
53082
|
-
if (authStore.gheDomain && authStore.accountType === "individual") authStore.accountType = "enterprise";
|
|
53443
|
+
applyGheDomain(authStore, cachedConfig.gheDomain, options.gheDomain);
|
|
53083
53444
|
await cacheVSCodeVersion();
|
|
53084
53445
|
if (!options.githubToken) await setupGitHubToken();
|
|
53085
53446
|
const tokenCleanup = await setupCopilotToken();
|
|
@@ -53194,15 +53555,15 @@ const start = defineCommand({
|
|
|
53194
53555
|
},
|
|
53195
53556
|
"upstream-queue-retries": {
|
|
53196
53557
|
type: "string",
|
|
53197
|
-
description: "Maximum retries for upstream
|
|
53558
|
+
description: "Maximum retries for transient upstream responses (default: 5)"
|
|
53198
53559
|
},
|
|
53199
53560
|
"upstream-queue-base-delay": {
|
|
53200
53561
|
type: "string",
|
|
53201
|
-
description: "Base delay in seconds for upstream
|
|
53562
|
+
description: "Base delay in seconds for upstream retry backoff when Retry-After is absent (default: 2)"
|
|
53202
53563
|
},
|
|
53203
53564
|
"upstream-queue-max-delay": {
|
|
53204
53565
|
type: "string",
|
|
53205
|
-
description: "Maximum delay in seconds for upstream
|
|
53566
|
+
description: "Maximum delay in seconds for upstream retry backoff (default: 60)"
|
|
53206
53567
|
},
|
|
53207
53568
|
"ghe-domain": {
|
|
53208
53569
|
alias: "ghe",
|