@dianshuv/copilot-api 0.7.10 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.mjs +257 -36
  2. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -243,7 +243,7 @@ function compressToolResultContent(content) {
243
243
  return `${start}\n\n[... ${(content.length - COMPRESSED_SUMMARY_LENGTH).toLocaleString()} characters omitted for brevity ...]\n\n${end}`;
244
244
  }
245
245
  function calculateLimits(model, config, defaultContextWindow) {
246
- const rawTokenLimit = getEffectiveTokenLimit(model.id) ?? model.capabilities?.limits?.max_context_window_tokens ?? model.capabilities?.limits?.max_prompt_tokens ?? defaultContextWindow;
246
+ const rawTokenLimit = getEffectiveTokenLimit(config.tokenLimitCacheKeyOverride ?? model.id) ?? config.contextWindowOverride ?? model.capabilities?.limits?.max_context_window_tokens ?? model.capabilities?.limits?.max_prompt_tokens ?? defaultContextWindow;
247
247
  return {
248
248
  tokenLimit: Math.floor(rawTokenLimit * (1 - config.safetyMarginPercent / 100)),
249
249
  byteLimit: getEffectiveByteLimitBytes()
@@ -1348,7 +1348,7 @@ const patchClaude = defineCommand({
1348
1348
 
1349
1349
  //#endregion
1350
1350
  //#region package.json
1351
- var version = "0.7.10";
1351
+ var version = "0.8.1";
1352
1352
 
1353
1353
  //#endregion
1354
1354
  //#region src/lib/adaptive-rate-limiter.ts
@@ -3395,6 +3395,89 @@ const getTokenCount = async (payload, model) => {
3395
3395
  };
3396
3396
  };
3397
3397
 
3398
+ //#endregion
3399
+ //#region src/lib/anthropic/beta.ts
3400
+ /**
3401
+ * Vendor-neutral utilities for manipulating the `anthropic-beta` request header.
3402
+ *
3403
+ * Lives in `lib/anthropic/` (not in either transport module) so both the
3404
+ * Anthropic-native and OpenAI-translated transport layers can share these
3405
+ * helpers without introducing cross-transport imports.
3406
+ */
3407
+ /** Anthropic beta feature that unlocks the 1M context window. */
3408
+ const CONTEXT_1M_BETA_FEATURE = "context-1m-2025-08-07";
3409
+ /**
3410
+ * Merge two comma-separated anthropic-beta header values. Trims whitespace,
3411
+ * drops empty tokens, and dedupes by exact string match. Returns a canonical
3412
+ * comma-joined string with no spaces.
3413
+ *
3414
+ * Either input may be undefined / empty.
3415
+ */
3416
+ function mergeBetaFeatures(existing, incoming) {
3417
+ const seen = /* @__PURE__ */ new Set();
3418
+ const out = [];
3419
+ for (const raw of [existing, incoming]) {
3420
+ if (!raw) continue;
3421
+ for (const part of raw.split(",")) {
3422
+ const f = part.trim();
3423
+ if (f.length === 0 || seen.has(f)) continue;
3424
+ seen.add(f);
3425
+ out.push(f);
3426
+ }
3427
+ }
3428
+ return out.join(",");
3429
+ }
3430
+ /**
3431
+ * Append the context-1m feature to an anthropic-beta header value, deduping
3432
+ * any prior occurrence. Returns the merged comma-separated string.
3433
+ */
3434
+ function appendContext1mBeta(existing) {
3435
+ return mergeBetaFeatures(existing, CONTEXT_1M_BETA_FEATURE);
3436
+ }
3437
+ /**
3438
+ * True iff a model id appears to be the suffixed 1M-context variant of an
3439
+ * Anthropic Claude model (e.g. claude-opus-4-8-1m, claude-opus-4.6-1m).
3440
+ *
3441
+ * Used as a state.models-independent signal for whether to inject the
3442
+ * context-1m-2025-08-07 beta header, so the 1M intent survives a stale or
3443
+ * empty model cache (where `resolveAnthropicModelForDirectPath` would return
3444
+ * undefined). Forwarding the beta is harmless to upstreams that ignore it.
3445
+ */
3446
+ function isOneMillionSuffixedClaudeId(modelId) {
3447
+ return modelId.startsWith("claude-") && modelId.endsWith("-1m");
3448
+ }
3449
+
3450
+ //#endregion
3451
+ //#region src/lib/headers.ts
3452
+ /**
3453
+ * Vendor-neutral header-bag helpers.
3454
+ *
3455
+ * HTTP header names are case-insensitive, but a plain-object header bag is
3456
+ * case-sensitive on its keys. Code that wants to look up "anthropic-beta"
3457
+ * without knowing whether some other producer wrote "Anthropic-Beta" needs
3458
+ * `findHeaderKey`. Code that wants to set a header without creating a
3459
+ * second case variant of the same name needs `setHeader`.
3460
+ */
3461
+ /** Case-insensitive lookup of a header key in a plain-object header bag. */
3462
+ function findHeaderKey(headers, name) {
3463
+ const lower = name.toLowerCase();
3464
+ return Object.keys(headers).find((k) => k.toLowerCase() === lower);
3465
+ }
3466
+ /** Case-insensitive read of a header value. */
3467
+ function getHeader(headers, name) {
3468
+ const key = findHeaderKey(headers, name);
3469
+ return key === void 0 ? void 0 : headers[key];
3470
+ }
3471
+ /**
3472
+ * Set a header value at the existing case variant if one is present, else at
3473
+ * the supplied canonical name. Prevents a second key (different case) from
3474
+ * being added for the same logical header.
3475
+ */
3476
+ function setHeader(headers, name, value) {
3477
+ const key = findHeaderKey(headers, name) ?? name;
3478
+ headers[key] = value;
3479
+ }
3480
+
3398
3481
  //#endregion
3399
3482
  //#region src/services/copilot/create-chat-completions.ts
3400
3483
  const GPT_MODEL_PATTERN = /^gpt-/i;
@@ -3415,21 +3498,27 @@ const createChatCompletions = async (payload, options) => {
3415
3498
  const enableVision = wire.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
3416
3499
  const isAgentCall = wire.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
3417
3500
  const modelSupportsVision = options?.resolvedModel?.capabilities?.supports?.vision !== false;
3501
+ const headers = {
3502
+ ...copilotHeaders(state, {
3503
+ vision: enableVision && modelSupportsVision,
3504
+ modelRequestHeaders: options?.resolvedModel?.request_headers,
3505
+ intent: isAgentCall ? "conversation-agent" : "conversation-panel"
3506
+ }),
3507
+ "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
3508
+ };
3509
+ if (options?.anthropicBeta) {
3510
+ const existingKey = findHeaderKey(headers, "anthropic-beta") ?? "anthropic-beta";
3511
+ headers[existingKey] = mergeBetaFeatures(headers[existingKey], options.anthropicBeta);
3512
+ consola.debug(`[ChatCompletions] anthropic-beta after merge: ${headers[existingKey]}`);
3513
+ }
3418
3514
  const response = await copilotFetch("/chat/completions", {
3419
3515
  method: "POST",
3420
- headers: {
3421
- ...copilotHeaders(state, {
3422
- vision: enableVision && modelSupportsVision,
3423
- modelRequestHeaders: options?.resolvedModel?.request_headers,
3424
- intent: isAgentCall ? "conversation-agent" : "conversation-panel"
3425
- }),
3426
- "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
3427
- },
3516
+ headers,
3428
3517
  body: JSON.stringify(wire)
3429
3518
  });
3430
3519
  if (!response.ok) {
3431
3520
  consola.error("Failed to create chat completions", response);
3432
- throw await HTTPError.fromResponse("Failed to create chat completions", response, payload.model);
3521
+ throw await HTTPError.fromResponse("Failed to create chat completions", response, options?.errorModelIdOverride ?? payload.model);
3433
3522
  }
3434
3523
  if (payload.stream) return events(response);
3435
3524
  return await response.json();
@@ -3964,7 +4053,7 @@ function isNonStreaming(response) {
3964
4053
  return Object.hasOwn(response, "choices");
3965
4054
  }
3966
4055
  /** Build final payload with auto-truncate if needed */
3967
- async function buildFinalPayload(payload, model) {
4056
+ async function buildFinalPayload(payload, model, autoTruncateConfig = {}) {
3968
4057
  if (!state.autoTruncate || !model) {
3969
4058
  if (state.autoTruncate && !model) consola.warn(`Auto-truncate: Model '${payload.model}' not found in cached models, skipping`);
3970
4059
  return {
@@ -3973,7 +4062,7 @@ async function buildFinalPayload(payload, model) {
3973
4062
  };
3974
4063
  }
3975
4064
  try {
3976
- const check = await checkNeedsCompactionOpenAI(payload, model);
4065
+ const check = await checkNeedsCompactionOpenAI(payload, model, autoTruncateConfig);
3977
4066
  consola.debug(`Auto-truncate check: ${check.currentTokens} tokens (limit ${check.tokenLimit}), ${Math.round(check.currentBytes / 1024)}KB (limit ${check.byteLimit === Infinity ? "unlimited" : `${Math.round(check.byteLimit / 1024)}KB`}), needed: ${check.needed}${check.reason ? ` (${check.reason})` : ""}`);
3978
4067
  if (!check.needed) return {
3979
4068
  finalPayload: payload,
@@ -3984,7 +4073,7 @@ async function buildFinalPayload(payload, model) {
3984
4073
  else if (check.reason === "bytes") reasonText = "size";
3985
4074
  else reasonText = "tokens";
3986
4075
  consola.info(`Auto-truncate triggered: exceeds ${reasonText} limit`);
3987
- const truncateResult = await autoTruncateOpenAI(payload, model);
4076
+ const truncateResult = await autoTruncateOpenAI(payload, model, autoTruncateConfig);
3988
4077
  return {
3989
4078
  finalPayload: truncateResult.payload,
3990
4079
  truncateResult
@@ -6839,7 +6928,7 @@ function modelSupportsContextEditing(modelId) {
6839
6928
  }
6840
6929
  function modelSupportsToolSearch(modelId) {
6841
6930
  const n = normalizeForMatching(modelId);
6842
- return n.includes("claude") && (n.includes("opus45") || n.includes("opus46") || n.includes("sonnet45") || n.includes("sonnet46"));
6931
+ return n.includes("claude") && (n.includes("opus45") || n.includes("opus46") || n.includes("opus47") || n.includes("opus48") || n.includes("sonnet45") || n.includes("sonnet46"));
6843
6932
  }
6844
6933
  function isContextEditingEnabled(modelId) {
6845
6934
  return modelSupportsContextEditing(modelId) && state.contextEditingMode !== "off";
@@ -7058,14 +7147,18 @@ async function createAnthropicMessages(payload, options) {
7058
7147
  const headers = {
7059
7148
  ...copilotHeaders(state, {
7060
7149
  vision: enableVision,
7061
- intent: isAgentCall ? "conversation-agent" : "conversation-panel"
7150
+ intent: isAgentCall ? "conversation-agent" : "conversation-panel",
7151
+ modelRequestHeaders: resolvedModel?.request_headers
7062
7152
  }),
7063
7153
  "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user"),
7064
7154
  "anthropic-version": "2023-06-01"
7065
7155
  };
7066
- const betaHeaders = buildAnthropicBetaHeaders(filteredPayload.model, resolvedModel);
7067
- Object.assign(headers, betaHeaders);
7068
- if (isContextEditingEnabled(filteredPayload.model)) {
7156
+ const proxyBeta = buildAnthropicBetaHeaders(filteredPayload.model, resolvedModel)["anthropic-beta"];
7157
+ let mergedBeta = mergeBetaFeatures(getHeader(headers, "anthropic-beta"), proxyBeta);
7158
+ if (options?.injectContext1mBeta) mergedBeta = appendContext1mBeta(mergedBeta);
7159
+ if (options?.clientAnthropicBetaHeader) mergedBeta = mergeBetaFeatures(mergedBeta, options.clientAnthropicBetaHeader);
7160
+ if (mergedBeta.length > 0) setHeader(headers, "anthropic-beta", mergedBeta);
7161
+ if (!("context_management" in filteredPayload) && isContextEditingEnabled(filteredPayload.model)) {
7069
7162
  const hasThinking = filteredPayload.thinking?.type === "enabled";
7070
7163
  const cm = buildContextManagement(state.contextEditingMode, hasThinking);
7071
7164
  if (cm) {
@@ -7091,7 +7184,7 @@ async function createAnthropicMessages(payload, options) {
7091
7184
  thinking: filteredPayload.thinking,
7092
7185
  messageCount: filteredPayload.messages.length
7093
7186
  });
7094
- throw await HTTPError.fromResponse("Failed to create Anthropic messages", response, filteredPayload.model);
7187
+ throw await HTTPError.fromResponse("Failed to create Anthropic messages", response, options?.errorModelIdOverride ?? filteredPayload.model);
7095
7188
  }
7096
7189
  if (payload.stream) return events(response);
7097
7190
  return await response.json();
@@ -7115,12 +7208,71 @@ function stripServerToolsFromPayload(tools) {
7115
7208
  return result.length > 0 ? result : void 0;
7116
7209
  }
7117
7210
  /**
7211
+ * Effective 1M context window. Two ways a request lands on this size:
7212
+ * (a) base model id + context-1m-2025-08-07 beta header (e.g. claude-opus-4.8),
7213
+ * (b) a distinct upstream "-1m-internal" model id (e.g. claude-opus-4.7-1m-internal).
7214
+ */
7215
+ const ONE_MILLION_CONTEXT_WINDOW_TOKENS = 1e6;
7216
+ /**
7217
+ * Convert a Claude model id from the client-facing dash convention to the
7218
+ * upstream Copilot dot convention. The two conventions co-exist because
7219
+ * Anthropic-style clients use dashes ("claude-opus-4-8") and Copilot lists
7220
+ * the same model with dots ("claude-opus-4.8"). Only the first dash inside
7221
+ * the version segment is converted (a model id like "claude-opus-4.8-1m"
7222
+ * already in dot form is returned unchanged).
7223
+ */
7224
+ function dashToDotClaudeId(modelId) {
7225
+ if (!modelId.startsWith("claude-")) return modelId;
7226
+ return modelId.replace(/^(claude-[a-z]+-)(\d+)-(\d+)/, "$1$2.$3");
7227
+ }
7228
+ function resolveAnthropicModelForDirectPath(modelId) {
7229
+ const exact = findModelById(modelId);
7230
+ if (exact?.vendor === "Anthropic") return {
7231
+ model: exact,
7232
+ baseModelId: modelId,
7233
+ oneMillionFallback: false,
7234
+ effectiveContextWindowTokens: exact.capabilities?.limits?.max_context_window_tokens ?? 2e5
7235
+ };
7236
+ const dotted = dashToDotClaudeId(modelId);
7237
+ if (dotted !== modelId) {
7238
+ const dottedExact = findModelById(dotted);
7239
+ if (dottedExact?.vendor === "Anthropic") return {
7240
+ model: dottedExact,
7241
+ baseModelId: dotted,
7242
+ oneMillionFallback: false,
7243
+ effectiveContextWindowTokens: dottedExact.capabilities?.limits?.max_context_window_tokens ?? 2e5
7244
+ };
7245
+ }
7246
+ if (modelId.endsWith("-1m")) {
7247
+ const baseId = modelId.slice(0, -3);
7248
+ const dottedBaseId = dashToDotClaudeId(baseId);
7249
+ for (const candidateId of [`${baseId}-1m-internal`, `${dottedBaseId}-1m-internal`]) {
7250
+ const internal = findModelById(candidateId);
7251
+ if (internal?.vendor === "Anthropic") return {
7252
+ model: internal,
7253
+ baseModelId: candidateId,
7254
+ oneMillionFallback: false,
7255
+ effectiveContextWindowTokens: ONE_MILLION_CONTEXT_WINDOW_TOKENS
7256
+ };
7257
+ }
7258
+ for (const candidateId of [baseId, dottedBaseId]) {
7259
+ const base = findModelById(candidateId);
7260
+ if (base?.vendor === "Anthropic") return {
7261
+ model: base,
7262
+ baseModelId: candidateId,
7263
+ oneMillionFallback: true,
7264
+ effectiveContextWindowTokens: ONE_MILLION_CONTEXT_WINDOW_TOKENS
7265
+ };
7266
+ }
7267
+ }
7268
+ }
7269
+ /**
7118
7270
  * Check if a model supports direct Anthropic API.
7119
7271
  * Returns true if redirect is disabled (direct API is on) and the model is from Anthropic vendor.
7120
7272
  */
7121
7273
  function supportsDirectAnthropicApi(modelId) {
7122
7274
  if (state.redirectAnthropic) return false;
7123
- return findModelById(modelId)?.vendor === "Anthropic";
7275
+ return resolveAnthropicModelForDirectPath(modelId) !== void 0;
7124
7276
  }
7125
7277
 
7126
7278
  //#endregion
@@ -7359,18 +7511,42 @@ function findLatestModel(familyPrefix, fallback) {
7359
7511
  const candidates = models.filter((m) => m.id.startsWith(familyPrefix));
7360
7512
  if (candidates.length === 0) return fallback;
7361
7513
  candidates.sort((a, b) => {
7362
- const versionA = extractVersion(a.id, familyPrefix);
7363
- return extractVersion(b.id, familyPrefix) - versionA;
7514
+ const [aMajor, aMinor] = extractVersion(a.id, familyPrefix);
7515
+ const [bMajor, bMinor] = extractVersion(b.id, familyPrefix);
7516
+ if (aMajor !== bMajor) return bMajor - aMajor;
7517
+ return bMinor - aMinor;
7364
7518
  });
7365
7519
  return candidates[0].id;
7366
7520
  }
7367
7521
  /**
7368
- * Extract numeric version from model ID.
7369
- * e.g., "claude-opus-4.5" with prefix "claude-opus" -> 4.5
7522
+ * Extract numeric [major, minor] version from a model id.
7523
+ *
7524
+ * Supports both naming conventions Anthropic/Copilot have used:
7525
+ * - dot: "claude-opus-4.5" → [4, 5]
7526
+ * - dash: "claude-opus-4-8" → [4, 8]
7527
+ * - dash double-digit: "claude-opus-4-10" → [4, 10]
7528
+ *
7529
+ * The dash form previously parsed as just the major via the regex
7530
+ * /^(\d+(?:\.\d+)?)/ (the dash stopped the match), which silently
7531
+ * downgraded dash-named candidates against any dot-named candidate in
7532
+ * findLatestModel. Parsing into a tuple also avoids the parseFloat
7533
+ * lossiness on double-digit minors ("4.10" → 4.1).
7534
+ *
7535
+ * Anything after the major/minor segment (date stamps, "-1m") is ignored.
7536
+ * The minor capture is bounded to 1-3 digits so that an 8-digit date suffix
7537
+ * directly after the major (e.g. "claude-opus-4-20250514") is NOT mistaken
7538
+ * for a minor version of 20_250_514 — without that bound, dated ids would
7539
+ * outrank legitimate dotted candidates like "claude-opus-4.8" in
7540
+ * findLatestModel's sort.
7541
+ *
7542
+ * Returns [0, 0] when no version can be extracted.
7370
7543
  */
7371
7544
  function extractVersion(modelId, prefix) {
7372
- const match = modelId.slice(prefix.length + 1).match(/^(\d+(?:\.\d+)?)/);
7373
- return match ? Number.parseFloat(match[1]) : 0;
7545
+ const match = modelId.slice(prefix.length + 1).match(/^(\d+)(?:[.-](\d{1,3}))?/);
7546
+ if (!match) return [0, 0];
7547
+ const major = Number.parseInt(match[1], 10);
7548
+ const rawMinor = match[2];
7549
+ return [major, rawMinor === void 0 ? 0 : Number.parseInt(rawMinor, 10) || 0];
7374
7550
  }
7375
7551
  function translateModelName(model) {
7376
7552
  const aliasMap = {
@@ -7384,6 +7560,8 @@ function translateModelName(model) {
7384
7560
  }
7385
7561
  if (/^claude-sonnet-4-5-\d+$/.test(model)) return "claude-sonnet-4.5";
7386
7562
  if (/^claude-sonnet-4-\d+$/.test(model)) return "claude-sonnet-4";
7563
+ if (model === "claude-opus-4-8-1m") return "claude-opus-4.8";
7564
+ if (model === "claude-opus-4-8") return "claude-opus-4.8";
7387
7565
  if (model === "claude-opus-4-7-1m") return "claude-opus-4.7-1m-internal";
7388
7566
  if (/^claude-opus-4-7$/.test(model)) return "claude-opus-4.7";
7389
7567
  if (model === "claude-opus-4-6-1m") return "claude-opus-4.6-1m";
@@ -7763,22 +7941,44 @@ function translateErrorToAnthropicErrorEvent(error) {
7763
7941
  */
7764
7942
  async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiatorOverride) {
7765
7943
  consola.debug("Using direct Anthropic API path for model:", anthropicPayload.model);
7766
- const selectedModel = findModelById(anthropicPayload.model);
7767
- let effectivePayload = anthropicPayload;
7944
+ const resolution = resolveAnthropicModelForDirectPath(anthropicPayload.model);
7945
+ const selectedModel = resolution?.model;
7946
+ const baseModelId = resolution?.baseModelId ?? anthropicPayload.model;
7947
+ const needsContext1mBeta = resolution?.oneMillionFallback ?? false;
7948
+ const originalModelId = anthropicPayload.model;
7949
+ const resolvedPayload = resolution && baseModelId !== originalModelId ? {
7950
+ ...anthropicPayload,
7951
+ model: baseModelId
7952
+ } : anthropicPayload;
7953
+ if (baseModelId !== originalModelId) {
7954
+ consola.debug(`[Anthropic] Mapping model for upstream: ${originalModelId} → ${baseModelId}${needsContext1mBeta ? " (+context-1m beta)" : ""}`);
7955
+ updateTrackerResolvedModel(ctx.trackingId, baseModelId);
7956
+ }
7957
+ const autoTruncateConfig = resolution && resolution.oneMillionFallback ? {
7958
+ contextWindowOverride: resolution.effectiveContextWindowTokens,
7959
+ tokenLimitCacheKeyOverride: originalModelId
7960
+ } : {};
7961
+ let effectivePayload = resolvedPayload;
7768
7962
  let truncateResult;
7769
7963
  if (state.autoTruncate && selectedModel) {
7770
- const check = await checkNeedsCompactionAnthropic(anthropicPayload, selectedModel);
7964
+ const check = await checkNeedsCompactionAnthropic(resolvedPayload, selectedModel, autoTruncateConfig);
7771
7965
  consola.debug(`[Anthropic] Auto-truncate check: ${check.currentTokens} tokens (limit ${check.tokenLimit}), ${Math.round(check.currentBytes / 1024)}KB (limit ${check.byteLimit === Infinity ? "unlimited" : `${Math.round(check.byteLimit / 1024)}KB`}), needed: ${check.needed}${check.reason ? ` (${check.reason})` : ""}`);
7772
7966
  if (check.needed) try {
7773
- truncateResult = await autoTruncateAnthropic(anthropicPayload, selectedModel);
7967
+ truncateResult = await autoTruncateAnthropic(resolvedPayload, selectedModel, autoTruncateConfig);
7774
7968
  if (truncateResult.wasCompacted) effectivePayload = truncateResult.payload;
7775
7969
  } catch (error) {
7776
7970
  consola.warn("[Anthropic] Auto-truncate failed, proceeding with original payload:", error instanceof Error ? error.message : error);
7777
7971
  }
7778
7972
  } else if (state.autoTruncate && !selectedModel) consola.debug(`[Anthropic] Model '${anthropicPayload.model}' not found, skipping auto-truncate`);
7779
7973
  if (state.manualApprove) await awaitApproval();
7974
+ const clientAnthropicBetaHeader = c.req.header("anthropic-beta");
7780
7975
  try {
7781
- const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createAnthropicMessages(effectivePayload, { initiator: initiatorOverride }));
7976
+ const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createAnthropicMessages(effectivePayload, {
7977
+ initiator: initiatorOverride,
7978
+ injectContext1mBeta: needsContext1mBeta,
7979
+ errorModelIdOverride: needsContext1mBeta ? originalModelId : void 0,
7980
+ clientAnthropicBetaHeader
7981
+ }));
7782
7982
  ctx.queueWaitMs = queueWaitMs;
7783
7983
  if (Symbol.asyncIterator in response) {
7784
7984
  consola.debug("Streaming response from Copilot (direct Anthropic)");
@@ -7976,17 +8176,33 @@ const parseSubagentMarkerFromSystemReminder = (text) => {
7976
8176
  * Handle completion using OpenAI translation path (legacy)
7977
8177
  */
7978
8178
  async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOverride) {
8179
+ const originalModelId = anthropicPayload.model;
8180
+ const hasOneMillionSuffix = isOneMillionSuffixedClaudeId(originalModelId);
8181
+ const oneMResolution = resolveAnthropicModelForDirectPath(originalModelId);
8182
+ const needsContext1mBeta = hasOneMillionSuffix || (oneMResolution?.oneMillionFallback ?? false);
7979
8183
  const { payload: translatedPayload, toolNameMapping } = translateToOpenAI(anthropicPayload);
7980
8184
  consola.debug("Translated OpenAI request payload:", JSON.stringify(translatedPayload));
7981
8185
  updateTrackerResolvedModel(ctx.trackingId, translatedPayload.model);
7982
8186
  const selectedModel = findModelById(translatedPayload.model);
7983
- const { finalPayload: openAIPayload, truncateResult } = await buildFinalPayload(translatedPayload, selectedModel);
8187
+ const autoTruncateConfig = oneMResolution?.oneMillionFallback ? {
8188
+ contextWindowOverride: oneMResolution.effectiveContextWindowTokens,
8189
+ tokenLimitCacheKeyOverride: originalModelId
8190
+ } : {};
8191
+ const { finalPayload: openAIPayload, truncateResult } = await buildFinalPayload(translatedPayload, selectedModel, autoTruncateConfig);
7984
8192
  if (truncateResult) ctx.truncateResult = truncateResult;
8193
+ let anthropicBeta = c.req.header("anthropic-beta");
8194
+ if (needsContext1mBeta) anthropicBeta = appendContext1mBeta(anthropicBeta);
7985
8195
  if (state.manualApprove) await awaitApproval();
8196
+ let errorModelIdOverride;
8197
+ if (autoTruncateConfig.tokenLimitCacheKeyOverride !== void 0) errorModelIdOverride = autoTruncateConfig.tokenLimitCacheKeyOverride;
8198
+ else if (needsContext1mBeta && selectedModel) errorModelIdOverride = selectedModel.id;
8199
+ else if (hasOneMillionSuffix) errorModelIdOverride = originalModelId;
7986
8200
  try {
7987
8201
  const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createChatCompletions(openAIPayload, {
7988
8202
  initiator: initiatorOverride,
7989
- resolvedModel: selectedModel
8203
+ resolvedModel: selectedModel,
8204
+ anthropicBeta,
8205
+ errorModelIdOverride
7990
8206
  }));
7991
8207
  ctx.queueWaitMs = queueWaitMs;
7992
8208
  if (isNonStreaming(response)) return handleNonStreamingResponse({
@@ -8254,10 +8470,15 @@ async function handleCountTokens(c) {
8254
8470
  consola.warn("Model not found, returning default token count");
8255
8471
  return c.json({ input_tokens: 1 });
8256
8472
  }
8473
+ const directResolution = resolveAnthropicModelForDirectPath(anthropicPayload.model);
8474
+ const autoTruncateConfig = directResolution?.oneMillionFallback ? {
8475
+ contextWindowOverride: directResolution.effectiveContextWindowTokens,
8476
+ tokenLimitCacheKeyOverride: anthropicPayload.model
8477
+ } : {};
8257
8478
  if (state.autoTruncate) {
8258
- const truncateCheck = await checkNeedsCompactionAnthropic(anthropicPayload, selectedModel);
8479
+ const truncateCheck = await checkNeedsCompactionAnthropic(anthropicPayload, selectedModel, autoTruncateConfig);
8259
8480
  if (truncateCheck.needed) {
8260
- const contextWindow = selectedModel.capabilities?.limits?.max_context_window_tokens ?? 2e5;
8481
+ const contextWindow = autoTruncateConfig.contextWindowOverride ?? selectedModel.capabilities?.limits?.max_context_window_tokens ?? 2e5;
8261
8482
  const inflatedTokens = Math.floor(contextWindow * .95);
8262
8483
  consola.debug(`[count_tokens] Would trigger auto-truncate: ${truncateCheck.currentTokens} tokens > ${truncateCheck.tokenLimit}, returning inflated count: ${inflatedTokens}`);
8263
8484
  return c.json({ input_tokens: inflatedTokens });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.7.10",
3
+ "version": "0.8.1",
4
4
  "description": "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!",
5
5
  "author": "dianshuv",
6
6
  "type": "module",