ghc-proxy 0.8.1 → 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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/dist/main.mjs +699 -359
  3. 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
- "claude-sonnet-4",
5905
- "claude-sonnet-4.5",
5906
- "claude-haiku-4.5"
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 + conversationDeletionFlags.size + inputItemDeletionFlags.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
- const conversationId = responseKeyFromConversation(conversation);
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: true,
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 (!(isTransientUpstreamStatus(status) && context.retryable === true && attempt < this.options.maxRetries)) {
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.gheDomain = getCachedConfig().gheDomain;
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.gheDomain = getCachedConfig().gheDomain;
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.8.1";
7422
+ const VERSION = "0.9.0";
7389
7423
  //#endregion
7390
7424
  //#region src/debug.ts
7391
7425
  function getRuntimeInfo() {
@@ -47917,6 +47951,20 @@ function inferReasoningEffort(budgetTokens) {
47917
47951
  if (budgetTokens <= 24e3) return "medium";
47918
47952
  return "high";
47919
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
+ }
47920
47968
  function inferModelFamily(model) {
47921
47969
  if (model.startsWith("claude")) return "claude";
47922
47970
  if (model.startsWith("gpt") || model.startsWith("o1") || model.startsWith("o3") || model.startsWith("o4")) return "gpt";
@@ -47930,8 +47978,9 @@ const baseProfile = {
47930
47978
  includeUsageOnStream: true,
47931
47979
  applyThinking(request) {
47932
47980
  const thinking = request.thinking;
47933
- if (!thinking || thinking.type === "disabled") return {};
47934
- return { reasoning_effort: inferReasoningEffort(thinking.type === "adaptive" ? 24e3 : thinking.budgetTokens) };
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) };
47935
47984
  }
47936
47985
  };
47937
47986
  const claudeProfile = {
@@ -47941,10 +47990,11 @@ const claudeProfile = {
47941
47990
  includeUsageOnStream: true,
47942
47991
  applyThinking(request) {
47943
47992
  const thinking = request.thinking;
47944
- if (!thinking || thinking.type === "disabled") return {};
47993
+ if (thinking?.type === "disabled") return {};
47994
+ if (!thinking) return request.outputEffort ? { reasoning_effort: request.outputEffort } : {};
47945
47995
  const budgetTokens = thinking.type === "adaptive" ? 24e3 : thinking.budgetTokens;
47946
47996
  return {
47947
- reasoning_effort: inferReasoningEffort(budgetTokens),
47997
+ reasoning_effort: resolveRequestEffort(request, budgetTokens),
47948
47998
  thinking_budget: budgetTokens
47949
47999
  };
47950
48000
  }
@@ -48469,6 +48519,7 @@ function buildCapiExecutionPlan(request, options = {}) {
48469
48519
  stream: request.stream,
48470
48520
  temperature: request.temperature,
48471
48521
  top_p: request.topP,
48522
+ ...request.topK != null ? { top_k: request.topK } : {},
48472
48523
  user: request.userId,
48473
48524
  tools: serializeTools(request.tools),
48474
48525
  tool_choice: serializeToolChoice(request.toolChoice),
@@ -48727,6 +48778,12 @@ const anthropicThinkingSchema = union([
48727
48778
  budget_tokens: number().int().positive()
48728
48779
  }).loose()
48729
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
+ */
48730
48787
  const anthropicOutputFormatSchema = object({
48731
48788
  type: literal("json_schema"),
48732
48789
  schema: jsonObjectSchema,
@@ -48734,16 +48791,24 @@ const anthropicOutputFormatSchema = object({
48734
48791
  description: string().nullable().optional(),
48735
48792
  strict: boolean().optional()
48736
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
+ */
48737
48802
  const anthropicOutputConfigSchema = object({
48738
48803
  effort: _enum([
48739
48804
  "low",
48740
48805
  "medium",
48741
48806
  "high",
48742
- "max",
48743
- "xhigh"
48807
+ "xhigh",
48808
+ "max"
48744
48809
  ]).nullable().optional(),
48745
48810
  format: anthropicOutputFormatSchema.optional()
48746
- }).strict();
48811
+ }).loose();
48747
48812
  const anthropicMessagesBasePayloadSchema = object({
48748
48813
  model: string().min(1),
48749
48814
  messages: array(anthropicMessageSchema).min(1),
@@ -48788,11 +48853,23 @@ function parseEmbeddingRequest(payload) {
48788
48853
  }
48789
48854
  //#endregion
48790
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
+ */
48791
48865
  const REASONING_EFFORT_VALUES = [
48866
+ "none",
48792
48867
  "minimal",
48793
48868
  "low",
48794
48869
  "medium",
48795
- "high"
48870
+ "high",
48871
+ "xhigh",
48872
+ "max"
48796
48873
  ];
48797
48874
  //#endregion
48798
48875
  //#region src/ingest/validation/openai-chat.ts
@@ -48906,9 +48983,19 @@ function parseOpenAIChatPayload(payload) {
48906
48983
  }
48907
48984
  //#endregion
48908
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();
48909
48995
  const responsesInputTextSchema = object({
48910
48996
  type: _enum(["input_text", "output_text"]),
48911
- text: string()
48997
+ text: string(),
48998
+ prompt_cache_breakpoint: promptCacheBreakpointSchema.nullable().optional()
48912
48999
  }).loose();
48913
49000
  const responsesInputImageSchema = object({
48914
49001
  type: literal("input_image"),
@@ -48919,7 +49006,8 @@ const responsesInputImageSchema = object({
48919
49006
  "high",
48920
49007
  "auto",
48921
49008
  "original"
48922
- ]).optional()
49009
+ ]).optional(),
49010
+ prompt_cache_breakpoint: promptCacheBreakpointSchema.nullable().optional()
48923
49011
  }).loose().superRefine((item, ctx) => {
48924
49012
  if (!item.image_url && !item.file_id) ctx.addIssue({
48925
49013
  code: "custom",
@@ -49097,14 +49185,7 @@ const responsesToolChoiceSchema = union([
49097
49185
  }).loose()
49098
49186
  ]);
49099
49187
  const responsesReasoningConfigSchema = object({
49100
- effort: _enum([
49101
- "none",
49102
- "minimal",
49103
- "low",
49104
- "medium",
49105
- "high",
49106
- "xhigh"
49107
- ]).nullable().optional(),
49188
+ effort: _enum(REASONING_EFFORT_VALUES).nullable().optional(),
49108
49189
  generate_summary: _enum([
49109
49190
  "auto",
49110
49191
  "concise",
@@ -49162,6 +49243,10 @@ function createResponsesPayloadSchema(options) {
49162
49243
  stream_options: object({ include_obfuscation: boolean().nullable().optional() }).loose().nullable().optional(),
49163
49244
  safety_identifier: string().nullable().optional(),
49164
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(),
49165
49250
  prompt_cache_retention: _enum(["in-memory", "24h"]).nullable().optional(),
49166
49251
  truncation: _enum(["auto", "disabled"]).nullable().optional(),
49167
49252
  parallel_tool_calls: boolean().nullable().optional(),
@@ -49338,154 +49423,9 @@ function createUpstreamSignalFromConfig(clientSignal) {
49338
49423
  return createUpstreamSignal(clientSignal, authStore.upstreamTimeoutSeconds !== void 0 ? authStore.upstreamTimeoutSeconds * 1e3 : void 0);
49339
49424
  }
49340
49425
  //#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
49426
  //#region src/transform/constants.ts
49425
49427
  const CONTEXT_BETA_RE = /^context-\d+[km]-/;
49426
49428
  //#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
49429
  //#region src/transform/model-rewrite.ts
49490
49430
  /**
49491
49431
  * Unified model rewrite: user rules → built-in normalization → pass-through.
@@ -49542,42 +49482,133 @@ function matchesGlob(pattern, value) {
49542
49482
  return new RegExp(`^${pattern.replace(GLOB_SPECIAL_RE, "\\$&").replace(GLOB_STAR_RE, ".*")}$`).test(value);
49543
49483
  }
49544
49484
  //#endregion
49545
- //#region src/transform/rewrite.ts
49546
- const rewriteStep = {
49547
- tag: "rewrite",
49548
- apply(input) {
49549
- const payload = input.payload;
49550
- const original = payload.model;
49551
- payload.model = input.model;
49552
- let result;
49553
- try {
49554
- result = applyModelRewrite(payload);
49555
- } finally {
49556
- payload.model = original;
49557
- }
49558
- if (!result.reason) return null;
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;
49559
49500
  return {
49560
- model: result.model,
49561
- tag: result.reason,
49562
- mutatePayload: (p) => {
49563
- p.model = result.model;
49564
- }
49501
+ originalModel,
49502
+ routedModel: smallModel,
49503
+ reason: "compact"
49565
49504
  };
49566
49505
  }
49567
- };
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
+ }
49568
49533
  //#endregion
49569
- //#region src/transform/beta-headers.ts
49570
- const COPILOT_UNSUPPORTED_BETA_RE = /^mid-conversation-system-\d{4}-\d{2}-\d{2}$/;
49571
- function processAnthropicBetaHeader(rawHeader) {
49572
- if (!rawHeader) return void 0;
49573
- const values = rawHeader.split(",").map((v) => v.trim()).filter(Boolean);
49574
- const filtered = [];
49575
- for (const value of values) {
49576
- if (CONTEXT_BETA_RE.test(value)) continue;
49577
- if (COPILOT_UNSUPPORTED_BETA_RE.test(value)) continue;
49578
- filtered.push(value);
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
+ }
49579
49566
  }
49580
- return filtered.length > 0 ? filtered.join(",") : void 0;
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
+ };
49581
49612
  }
49582
49613
  //#endregion
49583
49614
  //#region src/translator/responses/signature-codec.ts
@@ -49649,7 +49680,28 @@ function isOutputConfigEffort(value) {
49649
49680
  return OUTPUT_CONFIG_EFFORT_RANK.has(value);
49650
49681
  }
49651
49682
  function normalizeOutputConfigEffort(effort, model) {
49652
- const supportedEfforts = model?.capabilities.supports.reasoning_effort?.filter(isOutputConfigEffort);
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);
49653
49705
  if (!supportedEfforts?.length) return;
49654
49706
  if (supportedEfforts.includes(effort)) return effort;
49655
49707
  return supportedEfforts.reduce((highest, current) => {
@@ -49660,6 +49712,45 @@ function normalizeOutputConfigEffort(effort, model) {
49660
49712
  function hasOutputConfigFormat(payload) {
49661
49713
  return payload?.output_config?.format != null;
49662
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
+ }
49663
49754
  function budgetTokensToEffort(budget) {
49664
49755
  if (budget >= 24e3) return "high";
49665
49756
  if (budget >= 8e3) return "medium";
@@ -49685,6 +49776,15 @@ function convertEnabledThinkingToAdaptive(payload, model) {
49685
49776
  effort: budgetTokensToEffort(budget)
49686
49777
  };
49687
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
+ */
49688
49788
  function sanitizeOutputConfig(payload, model) {
49689
49789
  if (!payload.output_config) return;
49690
49790
  if (!modelCache.supportsOutputConfig(model)) {
@@ -49693,10 +49793,14 @@ function sanitizeOutputConfig(payload, model) {
49693
49793
  }
49694
49794
  const effort = payload.output_config.effort;
49695
49795
  if (effort == null) {
49796
+ if (payload.output_config.format) {
49797
+ delete payload.output_config.effort;
49798
+ return;
49799
+ }
49696
49800
  delete payload.output_config;
49697
49801
  return;
49698
49802
  }
49699
- payload.output_config = { effort: normalizeOutputConfigEffort(effort, model) ?? effort };
49803
+ payload.output_config.effort = normalizeOutputConfigEffort(effort, model) ?? effort;
49700
49804
  }
49701
49805
  function normalizeCacheControlBlock(obj) {
49702
49806
  if (obj.cache_control && typeof obj.cache_control === "object") obj.cache_control = { type: obj.cache_control.type };
@@ -49709,11 +49813,223 @@ function sanitizeCacheControl(payload) {
49709
49813
  }
49710
49814
  if (payload.tools) for (const tool of payload.tools) normalizeCacheControlBlock(tool);
49711
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
+ }
49712
49839
  //#endregion
49713
- //#region src/transform/index.ts
49714
- const messagesModelChain = composeModelTransforms(rewriteStep, modelPolicyStep);
49715
- const chatCompletionsModelChain = composeModelTransforms(rewriteStep);
49716
- const responsesModelChain = composeModelTransforms(rewriteStep);
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
+ }
49717
50033
  //#endregion
49718
50034
  //#region src/translator/anthropic/document.ts
49719
50035
  /** Trimmed text of a `content`-source document part, or undefined when it carries none. */
@@ -49911,6 +50227,7 @@ function normalizeAnthropicRequest(payload) {
49911
50227
  })),
49912
50228
  toolChoice: normalizeToolChoice(payload.tool_choice),
49913
50229
  thinking: normalizeThinking(payload.thinking),
50230
+ outputEffort: payload.output_config?.effort ?? void 0,
49914
50231
  serviceTier: payload.service_tier
49915
50232
  };
49916
50233
  }
@@ -50385,11 +50702,6 @@ function toConversationTurn(turn) {
50385
50702
  };
50386
50703
  }
50387
50704
  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
50705
  if (request.serviceTier !== void 0) context.record({
50394
50706
  kind: "unsupported_service_tier",
50395
50707
  severity: "warning",
@@ -50456,6 +50768,7 @@ function normalizeAnthropicConversation(payload, policy) {
50456
50768
  stream: normalized.stream,
50457
50769
  temperature: normalized.temperature,
50458
50770
  topP: normalized.topP,
50771
+ topK: normalized.topK,
50459
50772
  userId: normalized.userId,
50460
50773
  tools: normalized.tools?.map((tool) => ({
50461
50774
  name: tool.name,
@@ -50463,7 +50776,8 @@ function normalizeAnthropicConversation(payload, policy) {
50463
50776
  inputSchema: tool.inputSchema
50464
50777
  })),
50465
50778
  toolChoice: normalized.toolChoice,
50466
- thinking: normalized.thinking
50779
+ thinking: normalized.thinking,
50780
+ outputEffort: normalized.outputEffort ?? void 0
50467
50781
  },
50468
50782
  issues: context.getIssues()
50469
50783
  };
@@ -50785,7 +51099,6 @@ async function handleCompletionCore({ body, signal, headers }) {
50785
51099
  headers
50786
51100
  }, {
50787
51101
  protocol: "openai-chat",
50788
- transformChain: chatCompletionsModelChain,
50789
51102
  strategyRegistry: chatCompletionsStrategyRegistry,
50790
51103
  afterIngest({ payload }) {
50791
51104
  consola.debug("Request payload:", JSON.stringify(payload).slice(-400));
@@ -50804,6 +51117,7 @@ async function handleCompletionCore({ body, signal, headers }) {
50804
51117
  payload.max_tokens = selectedModel?.capabilities.limits.max_output_tokens;
50805
51118
  consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
50806
51119
  }
51120
+ applyChatCompletionsTokenParam(payload, selectedModel);
50807
51121
  },
50808
51122
  buildStrategyContext({ payload, meta, copilotClient, upstreamSignal, modelMapping }) {
50809
51123
  return {
@@ -50841,16 +51155,27 @@ function normalizeEmbeddingRequest(payload) {
50841
51155
  }
50842
51156
  /**
50843
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.
50844
51163
  */
50845
- async function handleEmbeddingsCore(body, headers) {
51164
+ async function handleEmbeddingsCore(body, headers, client, signal) {
50846
51165
  const { payload } = protocolRegistry.ingest("embeddings", body, headers);
50847
- return await createCopilotClient().createEmbeddings(normalizeEmbeddingRequest(payload));
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
+ }
50848
51173
  }
50849
51174
  //#endregion
50850
51175
  //#region src/routes/embeddings/route.ts
50851
51176
  function createEmbeddingRoutes() {
50852
51177
  return new Elysia().use(requestGuardPlugin).post("/embeddings", async ({ body, request }) => {
50853
- return handleEmbeddingsCore(body, request.headers);
51178
+ return handleEmbeddingsCore(body, request.headers, void 0, request.signal);
50854
51179
  }, { guarded: true });
50855
51180
  }
50856
51181
  //#endregion
@@ -50904,6 +51229,20 @@ async function handleCountTokensCore({ body, headers }) {
50904
51229
  return { input_tokens: finalTokenCount };
50905
51230
  }
50906
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
50907
51246
  //#region src/transform/context-management.ts
50908
51247
  /** Default token threshold when model limits are unknown. */
50909
51248
  const DEFAULT_COMPACT_THRESHOLD = 5e4;
@@ -50965,59 +51304,32 @@ function containsVisionContent(value) {
50965
51304
  return false;
50966
51305
  }
50967
51306
  //#endregion
50968
- //#region src/transform/parameter-filter.ts
50969
- /**
50970
- * Parameters the default rule strips for reasoning models on the Responses
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"];
51307
+ //#region src/transform/responses-input.ts
50976
51308
  /**
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.
51309
+ * Strip `phase` from input message items.
50987
51310
  *
50988
- * Rule composition:
50989
- * 1. Built-in default: reasoning models strip {@link DEFAULT_REASONING_UNSUPPORTED_PARAMS}.
50990
- * Disabled entirely when `responsesApiParameterFiltersReplaceDefault` is true.
50991
- * 2. User rules (`responsesApiParameterFilters`): every rule whose `models`
50992
- * 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`).
50993
51315
  *
50994
- * The result is the union of all matching rules, so user rules ADD to the
50995
- * default. Setting `responsesApiParameterFiltersReplaceDefault: true` disables
50996
- * the default so user rules fully OVERWRITE it.
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.
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.
51011
51320
  */
51012
- function applyResponsesParameterFilters(payload, model) {
51013
- const strip = resolveStrippedResponsesParams(model);
51014
- if (strip.size === 0) return;
51015
- const removed = [];
51016
- for (const key of strip) if (key in payload) {
51017
- delete payload[key];
51018
- 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
+ }
51019
51331
  }
51020
- if (removed.length > 0) consola.debug(`Stripped unsupported responses params for model ${model?.id}: ${removed.join(", ")}`);
51332
+ if (stripped > 0) consola.debug(`Stripped phase from ${stripped} input message item(s)`);
51021
51333
  }
51022
51334
  //#endregion
51023
51335
  //#region src/translator/responses/function-schema.ts
@@ -51239,6 +51551,7 @@ function translateAnthropicToResponsesPayload(payload, options) {
51239
51551
  instructions: translateSystemPrompt(payload.system),
51240
51552
  temperature: payload.temperature ?? null,
51241
51553
  top_p: payload.top_p ?? null,
51554
+ ...payload.top_k !== void 0 ? { top_k: payload.top_k } : {},
51242
51555
  max_output_tokens: payload.max_tokens,
51243
51556
  tools: convertAnthropicTools(payload.tools),
51244
51557
  tool_choice: convertAnthropicToolChoice(payload.tool_choice),
@@ -51408,25 +51721,49 @@ function resolveResponsesTextConfig(payload) {
51408
51721
  } };
51409
51722
  }
51410
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
+ */
51411
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) {
51412
51740
  if (payload.thinking?.type === "disabled") return "none";
51413
- if (payload.output_config?.effort) return mapAnthropicEffortToResponses(payload.output_config.effort);
51741
+ if (payload.output_config?.effort) return payload.output_config.effort;
51414
51742
  if (payload.thinking?.type === "adaptive") return "medium";
51415
51743
  if (payload.thinking?.type === "enabled") return options?.reasoningEffortResolver?.(payload.model) ?? "medium";
51416
51744
  }
51417
- function mapAnthropicEffortToResponses(effort) {
51418
- if (effort === "max") return "xhigh";
51419
- return effort;
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;
51420
51761
  }
51421
51762
  function assertResponsesCompatibleRequest(payload) {
51422
51763
  if (payload.stop_sequences?.length) throw new TranslationFailure("Anthropic stop_sequences cannot be forwarded through the Responses execution path.", {
51423
51764
  status: 400,
51424
51765
  kind: "unsupported_stop_sequences"
51425
51766
  });
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
51767
  if (payload.service_tier !== void 0) throw new TranslationFailure("Anthropic service_tier is not supported on the Responses execution path.", {
51431
51768
  status: 400,
51432
51769
  kind: "unsupported_service_tier"
@@ -51708,10 +52045,12 @@ function mapResponsesUsage(response) {
51708
52045
  const inputTokens = response.usage?.input_tokens ?? 0;
51709
52046
  const outputTokens = response.usage?.output_tokens ?? 0;
51710
52047
  const cachedTokens = response.usage?.input_tokens_details?.cached_tokens;
52048
+ const writtenTokens = response.usage?.input_tokens_details?.cache_write_tokens;
51711
52049
  return {
51712
52050
  input_tokens: inputTokens - (cachedTokens ?? 0),
51713
52051
  output_tokens: outputTokens,
51714
- ...cachedTokens !== void 0 ? { cache_read_input_tokens: cachedTokens } : {}
52052
+ ...cachedTokens !== void 0 ? { cache_read_input_tokens: cachedTokens } : {},
52053
+ ...writtenTokens ? { cache_creation_input_tokens: writtenTokens } : {}
51715
52054
  };
51716
52055
  }
51717
52056
  function isRecord$1(value) {
@@ -52164,12 +52503,15 @@ function createMessagesViaResponsesStrategy(copilotClient, responsesPayload, opt
52164
52503
  //#region src/routes/messages/strategy-registry.ts
52165
52504
  const nativeMessagesEntry = {
52166
52505
  name: "native-messages",
52167
- 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)),
52168
52507
  async execute(ctx) {
52169
52508
  convertEnabledThinkingToAdaptive(ctx.anthropicPayload, ctx.selectedModel);
52170
52509
  filterThinkingBlocksForNativeMessages(ctx.anthropicPayload);
52171
52510
  sanitizeOutputConfig(ctx.anthropicPayload, ctx.selectedModel);
52511
+ reduceOutputFormatForNativeMessages(ctx.anthropicPayload);
52512
+ sanitizeExclusiveSamplingParams(ctx.anthropicPayload);
52172
52513
  sanitizeCacheControl(ctx.anthropicPayload);
52514
+ clampMessagesOutputTokens(ctx.anthropicPayload, ctx.selectedModel);
52173
52515
  return await runStrategy(createNativeMessagesStrategy(ctx.copilotClient, ctx.anthropicPayload, ctx.anthropicBetaHeader, {
52174
52516
  signal: ctx.upstreamSignal.signal,
52175
52517
  requestContext: ctx.requestContext
@@ -52180,10 +52522,15 @@ const responsesApiEntry = {
52180
52522
  name: "responses-api",
52181
52523
  canHandle: (model) => modelCache.supportsEndpoint(model, RESPONSES_ENDPOINT),
52182
52524
  async execute(ctx) {
52183
- const responsesPayload = withTranslationErrors(() => translateAnthropicToResponsesPayload(ctx.anthropicPayload, { reasoningEffortResolver: (model) => configStore.getReasoningEffort(model) }));
52525
+ const responsesPayload = withTranslationErrors(() => translateAnthropicToResponsesPayload(ctx.anthropicPayload, {
52526
+ reasoningEffortResolver: (model) => configStore.getReasoningEffort(model),
52527
+ supportedEfforts: ctx.selectedModel?.capabilities.supports.reasoning_effort
52528
+ }));
52184
52529
  applyContextManagement(responsesPayload, ctx.selectedModel?.capabilities.limits.max_prompt_tokens);
52185
52530
  compactInputByLatestCompaction(responsesPayload);
52531
+ stripPhaseFromInputMessages(responsesPayload);
52186
52532
  applyResponsesParameterFilters(responsesPayload, ctx.selectedModel);
52533
+ clampResponsesOutputTokens(responsesPayload);
52187
52534
  const { vision, initiator } = getResponsesRequestOptions(responsesPayload);
52188
52535
  return await runStrategy(createMessagesViaResponsesStrategy(ctx.copilotClient, responsesPayload, {
52189
52536
  vision,
@@ -52220,7 +52567,7 @@ async function handleMessagesCore({ body, signal, headers }) {
52220
52567
  headers
52221
52568
  }, {
52222
52569
  protocol: "anthropic-messages",
52223
- transformChain: messagesModelChain,
52570
+ applyModelPolicy: true,
52224
52571
  strategyRegistry: defaultStrategyRegistry,
52225
52572
  afterIngest({ payload, headers: reqHeaders }) {
52226
52573
  if (consola.level >= 4) consola.debug("Anthropic request payload:", JSON.stringify(payload));
@@ -52265,9 +52612,13 @@ function createMessageRoutes() {
52265
52612
  //#region src/routes/models/handler.ts
52266
52613
  /**
52267
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.
52268
52619
  */
52269
- async function handleModelsCore() {
52270
- if (!modelCache.getModels()) await cacheModels(createCopilotClient());
52620
+ async function handleModelsCore(client) {
52621
+ if (!modelCache.getModels()) await cacheModels(client ?? createCopilotClient());
52271
52622
  return {
52272
52623
  object: "list",
52273
52624
  data: modelCache.getModels()?.data.map((model) => ({
@@ -52285,9 +52636,9 @@ async function handleModelsCore() {
52285
52636
  //#endregion
52286
52637
  //#region src/routes/models/route.ts
52287
52638
  function createModelRoutes() {
52288
- return new Elysia().get("/models", async () => {
52639
+ return new Elysia().use(requestGuardPlugin).get("/models", async () => {
52289
52640
  return handleModelsCore();
52290
- });
52641
+ }, { guarded: true });
52291
52642
  }
52292
52643
  //#endregion
52293
52644
  //#region src/routes/responses/emulator.ts
@@ -52667,7 +53018,6 @@ async function handleResponsesCore({ body, signal, headers }) {
52667
53018
  headers
52668
53019
  }, {
52669
53020
  protocol: "responses",
52670
- transformChain: responsesModelChain,
52671
53021
  strategyRegistry: responsesStrategyRegistry,
52672
53022
  afterIngest({ payload }) {
52673
53023
  originalPayload = payload;
@@ -52682,6 +53032,8 @@ async function handleResponsesCore({ body, signal, headers }) {
52682
53032
  if (!modelCache.supportsEndpoint(selectedModel, "/responses")) throwInvalidRequestError("The selected model does not support the responses endpoint.", "model");
52683
53033
  applyContextManagement(payload, selectedModel.capabilities.limits.max_prompt_tokens);
52684
53034
  applyResponsesParameterFilters(payload, selectedModel);
53035
+ clampResponsesOutputTokens(payload);
53036
+ clampResponsesReasoningEffort(payload, selectedModel);
52685
53037
  },
52686
53038
  buildStrategyContext({ payload, meta, copilotClient, upstreamSignal }) {
52687
53039
  const { vision, initiator } = getResponsesRequestOptions(payload);
@@ -52754,23 +53106,6 @@ function applyResponsesInputPolicies(payload) {
52754
53106
  rejectUnsupportedRemoteImageUrls(payload);
52755
53107
  }
52756
53108
  /**
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
53109
  * Remove input items that Copilot cannot resolve and would trigger 404:
52775
53110
  * - `item_reference` items (opaque IDs from store=true sessions)
52776
53111
  * - `function_call_output` items whose `call_id` has no matching prior
@@ -52843,35 +53178,42 @@ var UpstreamResourceDispatcher = class {
52843
53178
  return this.client.deleteResponse(responseId, options);
52844
53179
  }
52845
53180
  };
52846
- function createResourceDispatcher() {
52847
- return configStore.isEmulatorEnabled() ? new EmulatorResourceDispatcher() : new UpstreamResourceDispatcher(createCopilotClient());
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());
52848
53190
  }
52849
53191
  //#endregion
52850
53192
  //#region src/routes/responses/resource-handler.ts
52851
- async function handleRetrieveResponseCore({ params, url, headers, signal }) {
53193
+ async function handleRetrieveResponseCore({ params, url, headers, signal, client }) {
52852
53194
  const responseId = requireResponseId(params.responseId);
52853
- return await createResourceDispatcher().retrieve(responseId, getRetrieveParamsFromUrl(url), {
53195
+ return await createResourceDispatcher(client).retrieve(responseId, getRetrieveParamsFromUrl(url), {
52854
53196
  requestContext: readCapiRequestContext(headers),
52855
53197
  signal
52856
53198
  });
52857
53199
  }
52858
- async function handleListResponseInputItemsCore({ params, url, headers, signal }) {
53200
+ async function handleListResponseInputItemsCore({ params, url, headers, signal, client }) {
52859
53201
  const responseId = requireResponseId(params.responseId);
52860
- return await createResourceDispatcher().listInputItems(responseId, getInputItemsParamsFromUrl(url), {
53202
+ return await createResourceDispatcher(client).listInputItems(responseId, getInputItemsParamsFromUrl(url), {
52861
53203
  requestContext: readCapiRequestContext(headers),
52862
53204
  signal
52863
53205
  });
52864
53206
  }
52865
- async function handleCreateResponseInputTokensCore({ body, headers, signal }) {
53207
+ async function handleCreateResponseInputTokensCore({ body, headers, signal, client }) {
52866
53208
  const { payload, meta } = protocolRegistry.ingest("responses-input-tokens", body, headers);
52867
- return await createResourceDispatcher().createInputTokens(payload, {
53209
+ return await createResourceDispatcher(client).createInputTokens(payload, {
52868
53210
  requestContext: meta.requestContext,
52869
53211
  signal
52870
53212
  });
52871
53213
  }
52872
- async function handleDeleteResponseCore({ params, headers, signal }) {
53214
+ async function handleDeleteResponseCore({ params, headers, signal, client }) {
52873
53215
  const responseId = requireResponseId(params.responseId);
52874
- return await createResourceDispatcher().delete(responseId, {
53216
+ return await createResourceDispatcher(client).delete(responseId, {
52875
53217
  requestContext: readCapiRequestContext(headers),
52876
53218
  signal
52877
53219
  });
@@ -53098,9 +53440,7 @@ async function runServer(options) {
53098
53440
  baseDelayMs: secondsToMs(upstreamQueueBaseDelaySeconds),
53099
53441
  maxDelayMs: secondsToMs(upstreamQueueMaxDelaySeconds)
53100
53442
  });
53101
- authStore.gheDomain = cachedConfig.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";
53443
+ applyGheDomain(authStore, cachedConfig.gheDomain, options.gheDomain);
53104
53444
  await cacheVSCodeVersion();
53105
53445
  if (!options.githubToken) await setupGitHubToken();
53106
53446
  const tokenCleanup = await setupCopilotToken();