@dianshuv/copilot-api 0.19.0 → 0.20.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 +511 -995
  2. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -143,7 +143,6 @@ const state = {
143
143
  allowTokenEndpoint: false,
144
144
  autoTruncate: true,
145
145
  compressToolResults: false,
146
- redirectAnthropic: false,
147
146
  stripServerTools: false,
148
147
  contextEditingMode: "off",
149
148
  normalizeResponsesCallIds: true,
@@ -1012,7 +1011,7 @@ const logout = defineCommand({
1012
1011
 
1013
1012
  //#endregion
1014
1013
  //#region package.json
1015
- var version = "0.19.0";
1014
+ var version = "0.20.1";
1016
1015
 
1017
1016
  //#endregion
1018
1017
  //#region src/lib/event-loop-lag.ts
@@ -2450,6 +2449,8 @@ const HIDDEN_MODEL_IDS = new Set([
2450
2449
  "gpt-41-copilot",
2451
2450
  "gpt-5-mini",
2452
2451
  "gpt-5.3-codex",
2452
+ "gpt-5.4",
2453
+ "gpt-5.4-nano",
2453
2454
  "text-embedding-ada-002",
2454
2455
  "text-embedding-3-small",
2455
2456
  "text-embedding-3-small-inference",
@@ -2460,7 +2461,9 @@ const HIDDEN_MODEL_IDS = new Set([
2460
2461
  "claude-opus-4.7-high",
2461
2462
  "claude-opus-4.7-xhigh",
2462
2463
  "claude-sonnet-4.5",
2464
+ "claude-sonnet-4.6",
2463
2465
  "mai-code-1-flash-internal",
2466
+ "mai-code-1-flash-picker",
2464
2467
  "trajectory-compaction"
2465
2468
  ]);
2466
2469
  function isHiddenModel(id, showAll) {
@@ -3567,9 +3570,8 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
3567
3570
  *
3568
3571
  * Pre-flight steps for any request whose final payload is an OpenAI
3569
3572
  * ChatCompletionsPayload — auto-truncate decisions, 413 diagnostic logging,
3570
- * and the non-streaming type guard. Used by both `routes/chat-completions`
3571
- * (native OpenAI) and `routes/messages/translated-handler` (Anthropic that
3572
- * gets translated into OpenAI shape before hitting upstream).
3573
+ * and the non-streaming type guard. Used by `routes/chat-completions`
3574
+ * (native OpenAI).
3573
3575
  */
3574
3576
  /** Type guard for non-streaming responses */
3575
3577
  function isNonStreaming(response) {
@@ -3777,89 +3779,6 @@ function createStreamRepetitionChecker(label, config) {
3777
3779
  };
3778
3780
  }
3779
3781
 
3780
- //#endregion
3781
- //#region src/lib/anthropic/beta.ts
3782
- /**
3783
- * Vendor-neutral utilities for manipulating the `anthropic-beta` request header.
3784
- *
3785
- * Lives in `lib/anthropic/` (not in either transport module) so both the
3786
- * Anthropic-native and OpenAI-translated transport layers can share these
3787
- * helpers without introducing cross-transport imports.
3788
- */
3789
- /** Anthropic beta feature that unlocks the 1M context window. */
3790
- const CONTEXT_1M_BETA_FEATURE = "context-1m-2025-08-07";
3791
- /**
3792
- * Merge two comma-separated anthropic-beta header values. Trims whitespace,
3793
- * drops empty tokens, and dedupes by exact string match. Returns a canonical
3794
- * comma-joined string with no spaces.
3795
- *
3796
- * Either input may be undefined / empty.
3797
- */
3798
- function mergeBetaFeatures(existing, incoming) {
3799
- const seen = /* @__PURE__ */ new Set();
3800
- const out = [];
3801
- for (const raw of [existing, incoming]) {
3802
- if (!raw) continue;
3803
- for (const part of raw.split(",")) {
3804
- const f = part.trim();
3805
- if (f.length === 0 || seen.has(f)) continue;
3806
- seen.add(f);
3807
- out.push(f);
3808
- }
3809
- }
3810
- return out.join(",");
3811
- }
3812
- /**
3813
- * Append the context-1m feature to an anthropic-beta header value, deduping
3814
- * any prior occurrence. Returns the merged comma-separated string.
3815
- */
3816
- function appendContext1mBeta(existing) {
3817
- return mergeBetaFeatures(existing, CONTEXT_1M_BETA_FEATURE);
3818
- }
3819
- /**
3820
- * True iff a model id appears to be the suffixed 1M-context variant of an
3821
- * Anthropic Claude model (e.g. claude-opus-4-8-1m, claude-opus-4.6-1m).
3822
- *
3823
- * Used as a state.models-independent signal for whether to inject the
3824
- * context-1m-2025-08-07 beta header, so the 1M intent survives a stale or
3825
- * empty model cache (where `resolveAnthropicModelForDirectPath` would return
3826
- * undefined). Forwarding the beta is harmless to upstreams that ignore it.
3827
- */
3828
- function isOneMillionSuffixedClaudeId(modelId) {
3829
- return modelId.startsWith("claude-") && modelId.endsWith("-1m");
3830
- }
3831
-
3832
- //#endregion
3833
- //#region src/lib/headers.ts
3834
- /**
3835
- * Vendor-neutral header-bag helpers.
3836
- *
3837
- * HTTP header names are case-insensitive, but a plain-object header bag is
3838
- * case-sensitive on its keys. Code that wants to look up "anthropic-beta"
3839
- * without knowing whether some other producer wrote "Anthropic-Beta" needs
3840
- * `findHeaderKey`. Code that wants to set a header without creating a
3841
- * second case variant of the same name needs `setHeader`.
3842
- */
3843
- /** Case-insensitive lookup of a header key in a plain-object header bag. */
3844
- function findHeaderKey(headers, name) {
3845
- const lower = name.toLowerCase();
3846
- return Object.keys(headers).find((k) => k.toLowerCase() === lower);
3847
- }
3848
- /** Case-insensitive read of a header value. */
3849
- function getHeader(headers, name) {
3850
- const key = findHeaderKey(headers, name);
3851
- return key === void 0 ? void 0 : headers[key];
3852
- }
3853
- /**
3854
- * Set a header value at the existing case variant if one is present, else at
3855
- * the supplied canonical name. Prevents a second key (different case) from
3856
- * being added for the same logical header.
3857
- */
3858
- function setHeader(headers, name, value) {
3859
- const key = findHeaderKey(headers, name) ?? name;
3860
- headers[key] = value;
3861
- }
3862
-
3863
3782
  //#endregion
3864
3783
  //#region src/services/copilot/create-chat-completions.ts
3865
3784
  const GPT_MODEL_PATTERN = /^gpt-/i;
@@ -3880,21 +3799,15 @@ const createChatCompletions = async (payload, options) => {
3880
3799
  const enableVision = wire.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
3881
3800
  const isAgentCall = wire.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
3882
3801
  const modelSupportsVision = options?.resolvedModel?.capabilities?.supports?.vision !== false;
3883
- const headers = {
3884
- ...copilotHeaders(state, {
3885
- vision: enableVision && modelSupportsVision,
3886
- modelRequestHeaders: options?.resolvedModel?.request_headers
3887
- }),
3888
- "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
3889
- };
3890
- if (options?.anthropicBeta) {
3891
- const existingKey = findHeaderKey(headers, "anthropic-beta") ?? "anthropic-beta";
3892
- headers[existingKey] = mergeBetaFeatures(headers[existingKey], options.anthropicBeta);
3893
- consola.debug(`[ChatCompletions] anthropic-beta after merge: ${headers[existingKey]}`);
3894
- }
3895
3802
  const response = await copilotFetch("/chat/completions", {
3896
3803
  method: "POST",
3897
- headers,
3804
+ headers: {
3805
+ ...copilotHeaders(state, {
3806
+ vision: enableVision && modelSupportsVision,
3807
+ modelRequestHeaders: options?.resolvedModel?.request_headers
3808
+ }),
3809
+ "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
3810
+ },
3898
3811
  body: JSON.stringify(wire),
3899
3812
  signal: options?.signal
3900
3813
  });
@@ -4347,12 +4260,12 @@ async function executeRequest(opts) {
4347
4260
  signal: abort.signal
4348
4261
  }));
4349
4262
  ctx.queueWaitMs = queueWaitMs;
4350
- if (isNonStreaming(response)) return handleNonStreamingResponse$1(c, response, ctx, payload);
4263
+ if (isNonStreaming(response)) return handleNonStreamingResponse(c, response, ctx, payload);
4351
4264
  consola.debug("Streaming response");
4352
4265
  updateTrackerStatus(ctx.trackingId, "streaming");
4353
4266
  return streamSSE(c, async (stream) => {
4354
4267
  stream.onAbort(() => abort.abort());
4355
- await handleStreamingResponse$1({
4268
+ await handleStreamingResponse({
4356
4269
  stream,
4357
4270
  response,
4358
4271
  payload,
@@ -4381,7 +4294,7 @@ async function logTokenCount(payload, selectedModel) {
4381
4294
  consola.debug("Failed to calculate token count:", error);
4382
4295
  }
4383
4296
  }
4384
- function handleNonStreamingResponse$1(c, originalResponse, ctx, payload) {
4297
+ function handleNonStreamingResponse(c, originalResponse, ctx, payload) {
4385
4298
  consola.debug("Non-streaming response:", JSON.stringify(originalResponse));
4386
4299
  let response = originalResponse;
4387
4300
  if (state.verbose && ctx.truncateResult?.wasCompacted && response.choices[0]?.message.content) {
@@ -4474,7 +4387,7 @@ function createStreamAccumulator() {
4474
4387
  toolCallMap: /* @__PURE__ */ new Map()
4475
4388
  };
4476
4389
  }
4477
- async function handleStreamingResponse$1(opts) {
4390
+ async function handleStreamingResponse(opts) {
4478
4391
  const { stream, response, payload, ctx } = opts;
4479
4392
  const acc = createStreamAccumulator();
4480
4393
  const checkRepetition = createStreamRepetitionChecker(`openai:${payload.model}`);
@@ -6608,6 +6521,46 @@ async function checkNeedsCompactionAnthropic(payload, model, config = {}) {
6608
6521
  };
6609
6522
  }
6610
6523
 
6524
+ //#endregion
6525
+ //#region src/lib/anthropic/beta.ts
6526
+ /**
6527
+ * Vendor-neutral utilities for manipulating the `anthropic-beta` request header.
6528
+ *
6529
+ * Lives in `lib/anthropic/` (not in either transport module) so both the
6530
+ * Anthropic-native and OpenAI-translated transport layers can share these
6531
+ * helpers without introducing cross-transport imports.
6532
+ */
6533
+ /** Anthropic beta feature that unlocks the 1M context window. */
6534
+ const CONTEXT_1M_BETA_FEATURE = "context-1m-2025-08-07";
6535
+ /**
6536
+ * Merge two comma-separated anthropic-beta header values. Trims whitespace,
6537
+ * drops empty tokens, and dedupes by exact string match. Returns a canonical
6538
+ * comma-joined string with no spaces.
6539
+ *
6540
+ * Either input may be undefined / empty.
6541
+ */
6542
+ function mergeBetaFeatures(existing, incoming) {
6543
+ const seen = /* @__PURE__ */ new Set();
6544
+ const out = [];
6545
+ for (const raw of [existing, incoming]) {
6546
+ if (!raw) continue;
6547
+ for (const part of raw.split(",")) {
6548
+ const f = part.trim();
6549
+ if (f.length === 0 || seen.has(f)) continue;
6550
+ seen.add(f);
6551
+ out.push(f);
6552
+ }
6553
+ }
6554
+ return out.join(",");
6555
+ }
6556
+ /**
6557
+ * Append the context-1m feature to an anthropic-beta header value, deduping
6558
+ * any prior occurrence. Returns the merged comma-separated string.
6559
+ */
6560
+ function appendContext1mBeta(existing) {
6561
+ return mergeBetaFeatures(existing, CONTEXT_1M_BETA_FEATURE);
6562
+ }
6563
+
6611
6564
  //#endregion
6612
6565
  //#region src/lib/anthropic/features.ts
6613
6566
  function normalizeForMatching(modelId) {
@@ -6758,6 +6711,37 @@ function filterServerToolBlocksFromResponse(response) {
6758
6711
  };
6759
6712
  }
6760
6713
 
6714
+ //#endregion
6715
+ //#region src/lib/headers.ts
6716
+ /**
6717
+ * Vendor-neutral header-bag helpers.
6718
+ *
6719
+ * HTTP header names are case-insensitive, but a plain-object header bag is
6720
+ * case-sensitive on its keys. Code that wants to look up "anthropic-beta"
6721
+ * without knowing whether some other producer wrote "Anthropic-Beta" needs
6722
+ * `findHeaderKey`. Code that wants to set a header without creating a
6723
+ * second case variant of the same name needs `setHeader`.
6724
+ */
6725
+ /** Case-insensitive lookup of a header key in a plain-object header bag. */
6726
+ function findHeaderKey(headers, name) {
6727
+ const lower = name.toLowerCase();
6728
+ return Object.keys(headers).find((k) => k.toLowerCase() === lower);
6729
+ }
6730
+ /** Case-insensitive read of a header value. */
6731
+ function getHeader(headers, name) {
6732
+ const key = findHeaderKey(headers, name);
6733
+ return key === void 0 ? void 0 : headers[key];
6734
+ }
6735
+ /**
6736
+ * Set a header value at the existing case variant if one is present, else at
6737
+ * the supplied canonical name. Prevents a second key (different case) from
6738
+ * being added for the same logical header.
6739
+ */
6740
+ function setHeader(headers, name, value) {
6741
+ const key = findHeaderKey(headers, name) ?? name;
6742
+ headers[key] = value;
6743
+ }
6744
+
6761
6745
  //#endregion
6762
6746
  //#region src/services/copilot/create-anthropic-messages.ts
6763
6747
  /**
@@ -6960,11 +6944,12 @@ function resolveAnthropicModelForDirectPath(modelId) {
6960
6944
  }
6961
6945
  }
6962
6946
  /**
6963
- * Check if a model supports direct Anthropic API.
6964
- * Returns true if redirect is disabled (direct API is on) and the model is from Anthropic vendor.
6947
+ * Check if a model supports the native direct Anthropic API on Copilot.
6948
+ * True iff the model resolves to an Anthropic-vendor model (see
6949
+ * resolveAnthropicModelForDirectPath). `/v1/messages` serves these only;
6950
+ * the OpenAI-translation fallback was removed (docs/adr/0004-...).
6965
6951
  */
6966
6952
  function supportsDirectAnthropicApi(modelId) {
6967
- if (state.redirectAnthropic) return false;
6968
6953
  return resolveAnthropicModelForDirectPath(modelId) !== void 0;
6969
6954
  }
6970
6955
 
@@ -7161,15 +7146,6 @@ function extractToolCallsFromContent(content) {
7161
7146
  });
7162
7147
  return tools.length > 0 ? tools : void 0;
7163
7148
  }
7164
- function mapOpenAIStopReasonToAnthropic(finishReason) {
7165
- if (finishReason === null) return null;
7166
- return {
7167
- stop: "end_turn",
7168
- length: "max_tokens",
7169
- tool_calls: "tool_use",
7170
- content_filter: "end_turn"
7171
- }[finishReason];
7172
- }
7173
7149
  function prependMarkerToResponse(response, marker) {
7174
7150
  if (!marker) return response;
7175
7151
  const content = [...response.content];
@@ -7290,555 +7266,79 @@ function recordAnthropicStreamingResponse(acc, fallbackModel, ctx) {
7290
7266
  }
7291
7267
 
7292
7268
  //#endregion
7293
- //#region src/routes/messages/non-stream-translation.ts
7294
- const OPENAI_TOOL_NAME_LIMIT = 64;
7269
+ //#region src/routes/messages/stream-translation.ts
7295
7270
  /**
7296
- * Ensure all tool_use blocks have corresponding tool_result responses.
7297
- * This handles edge cases where conversation history may be incomplete:
7298
- * - Session interruptions where tool execution was cut off
7299
- * - Previous request failures
7300
- * - Client sending truncated history
7271
+ * Wrap an arbitrary error into an Anthropic-native `error` stream event.
7301
7272
  *
7302
- * Adding placeholder responses prevents API errors and maintains protocol compliance.
7273
+ * Shared by the direct Anthropic path (`direct-anthropic-handler.ts`) to emit a
7274
+ * client-facing error frame mid-stream. The OpenAI→Anthropic response
7275
+ * translation that once also lived here was removed with the translation
7276
+ * fallback (see docs/adr/0004-drop-openai-translation-fallback-for-messages.md).
7303
7277
  */
7304
- function fixMessageSequence(messages) {
7305
- const fixedMessages = [];
7306
- for (let i = 0; i < messages.length; i++) {
7307
- const message = messages[i];
7308
- fixedMessages.push(message);
7309
- if (message.role === "assistant" && message.tool_calls && message.tool_calls.length > 0) {
7310
- const foundToolResponses = /* @__PURE__ */ new Set();
7311
- let j = i + 1;
7312
- while (j < messages.length && messages[j].role === "tool") {
7313
- const toolMessage = messages[j];
7314
- if (toolMessage.tool_call_id) foundToolResponses.add(toolMessage.tool_call_id);
7315
- j++;
7316
- }
7317
- for (const toolCall of message.tool_calls) if (!foundToolResponses.has(toolCall.id)) {
7318
- consola.debug(`Adding placeholder tool_result for ${toolCall.id}`);
7319
- fixedMessages.push({
7320
- role: "tool",
7321
- tool_call_id: toolCall.id,
7322
- content: "Tool execution was interrupted or failed."
7323
- });
7324
- }
7278
+ function translateErrorToAnthropicErrorEvent(error) {
7279
+ return {
7280
+ type: "error",
7281
+ error: {
7282
+ type: "api_error",
7283
+ message: error ? formatError(error) : "An unexpected error occurred during streaming."
7325
7284
  }
7285
+ };
7286
+ }
7287
+
7288
+ //#endregion
7289
+ //#region src/routes/messages/tool-call-recovery.ts
7290
+ const ENVELOPE = String.raw`(?:<(?:antml:)?function_calls>|call|count|court)`;
7291
+ const INVOKE_BODY = String.raw`<(?:antml:)?invoke\s+name="[^"]+">(?:(?!<(?:antml:)?invoke\b)[\s\S])*?</(?:antml:)?invoke>`;
7292
+ const LEAKED_REGION_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*(?:` + INVOKE_BODY + String.raw`\s*)+(?:</(?:antml:)?function_calls>)?|` + INVOKE_BODY + String.raw`)`, "g");
7293
+ const STARTS_WITH_ENVELOPE_RE = new RegExp(String.raw`^[ \t\n]*` + ENVELOPE);
7294
+ const INVOKE_OPENER_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*)?<(?:antml:)?invoke\b[^\n>]*>?`, "g");
7295
+ const LEAK_OPEN_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*)?<(?:antml:)?invoke\s+name="`, "g");
7296
+ const INVOKE_RE = /<(?:antml:)?invoke\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?invoke>/g;
7297
+ const PARAMETER_RE = /<(?:antml:)?parameter\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?parameter>/g;
7298
+ function coerceParamValue(raw) {
7299
+ const trimmed = raw.trim();
7300
+ if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) try {
7301
+ return JSON.parse(trimmed);
7302
+ } catch {
7303
+ return raw;
7326
7304
  }
7327
- return fixedMessages;
7305
+ return raw;
7328
7306
  }
7329
- function translateToOpenAI(payload) {
7330
- const toolNameMapping = {
7331
- truncatedToOriginal: /* @__PURE__ */ new Map(),
7332
- originalToTruncated: /* @__PURE__ */ new Map()
7333
- };
7334
- const messages = translateAnthropicMessagesToOpenAI(payload.messages, payload.system, toolNameMapping);
7335
- return {
7336
- payload: {
7337
- model: translateModelName(payload.model),
7338
- messages: fixMessageSequence(messages),
7339
- max_tokens: payload.max_tokens,
7340
- stop: payload.stop_sequences,
7341
- stream: payload.stream,
7342
- temperature: payload.temperature,
7343
- top_p: payload.top_p,
7344
- user: payload.metadata?.user_id,
7345
- tools: translateAnthropicToolsToOpenAI(payload.tools, toolNameMapping),
7346
- tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice, toolNameMapping)
7347
- },
7348
- toolNameMapping
7349
- };
7307
+ function parseRegionInvokes(region, knownTools) {
7308
+ const calls = [];
7309
+ for (const invokeMatch of region.matchAll(INVOKE_RE)) {
7310
+ const name = invokeMatch[1];
7311
+ if (knownTools && !knownTools.has(name)) continue;
7312
+ const input = {};
7313
+ for (const paramMatch of invokeMatch[2].matchAll(PARAMETER_RE)) input[paramMatch[1]] = coerceParamValue(paramMatch[2]);
7314
+ calls.push({
7315
+ name,
7316
+ input
7317
+ });
7318
+ }
7319
+ return calls;
7320
+ }
7321
+ const FENCE_DELIM_RE = /(?:^|\n)[ \t]*```/g;
7322
+ function insideFence(text, pos) {
7323
+ let openAt = -1;
7324
+ for (const m of text.matchAll(FENCE_DELIM_RE)) if (openAt === -1) {
7325
+ if (m.index >= pos) break;
7326
+ openAt = m.index;
7327
+ } else if (m.index > pos) return true;
7328
+ else openAt = -1;
7329
+ return false;
7330
+ }
7331
+ function regionIsLeak(region, offset, fullText, knownTools) {
7332
+ if (insideFence(fullText, offset)) return false;
7333
+ if (STARTS_WITH_ENVELOPE_RE.test(region)) return true;
7334
+ if (!knownTools) return false;
7335
+ return parseRegionInvokes(region, knownTools).length > 0;
7350
7336
  }
7351
7337
  /**
7352
- * Find the latest available model matching a family prefix.
7353
- * Searches state.models for models starting with the given prefix
7354
- * and returns the one with the highest version number.
7355
- *
7356
- * @param familyPrefix - e.g., "claude-opus", "claude-sonnet", "claude-haiku"
7357
- * @param fallback - fallback model ID if no match found
7358
- */
7359
- function findLatestModel(familyPrefix, fallback) {
7360
- const models = state.models?.data;
7361
- if (!models || models.length === 0) return fallback;
7362
- const candidates = models.filter((m) => m.id.startsWith(familyPrefix));
7363
- if (candidates.length === 0) return fallback;
7364
- candidates.sort((a, b) => {
7365
- const [aMajor, aMinor] = extractVersion(a.id, familyPrefix);
7366
- const [bMajor, bMinor] = extractVersion(b.id, familyPrefix);
7367
- if (aMajor !== bMajor) return bMajor - aMajor;
7368
- return bMinor - aMinor;
7369
- });
7370
- return candidates[0].id;
7371
- }
7372
- /**
7373
- * Extract numeric [major, minor] version from a model id.
7374
- *
7375
- * Supports both naming conventions Anthropic/Copilot have used:
7376
- * - dot: "claude-opus-4.5" → [4, 5]
7377
- * - dash: "claude-opus-4-8" → [4, 8]
7378
- * - dash double-digit: "claude-opus-4-10" → [4, 10]
7379
- *
7380
- * The dash form previously parsed as just the major via the regex
7381
- * /^(\d+(?:\.\d+)?)/ (the dash stopped the match), which silently
7382
- * downgraded dash-named candidates against any dot-named candidate in
7383
- * findLatestModel. Parsing into a tuple also avoids the parseFloat
7384
- * lossiness on double-digit minors ("4.10" → 4.1).
7385
- *
7386
- * Anything after the major/minor segment (date stamps, "-1m") is ignored.
7387
- * The minor capture is bounded to 1-3 digits so that an 8-digit date suffix
7388
- * directly after the major (e.g. "claude-opus-4-20250514") is NOT mistaken
7389
- * for a minor version of 20_250_514 — without that bound, dated ids would
7390
- * outrank legitimate dotted candidates like "claude-opus-4.8" in
7391
- * findLatestModel's sort.
7392
- *
7393
- * Returns [0, 0] when no version can be extracted.
7394
- */
7395
- function extractVersion(modelId, prefix) {
7396
- const match = modelId.slice(prefix.length + 1).match(/^(\d+)(?:[.-](\d{1,3}))?/);
7397
- if (!match) return [0, 0];
7398
- const major = Number.parseInt(match[1], 10);
7399
- const rawMinor = match[2];
7400
- return [major, rawMinor === void 0 ? 0 : Number.parseInt(rawMinor, 10) || 0];
7401
- }
7402
- function translateModelName(model) {
7403
- const aliasMap = {
7404
- opus: "claude-opus",
7405
- sonnet: "claude-sonnet",
7406
- haiku: "claude-haiku"
7407
- };
7408
- if (aliasMap[model]) {
7409
- const familyPrefix = aliasMap[model];
7410
- return findLatestModel(familyPrefix, `${familyPrefix}-4.5`);
7411
- }
7412
- if (/^claude-sonnet-4-5-\d+$/.test(model)) return "claude-sonnet-4.5";
7413
- if (/^claude-sonnet-4-\d+$/.test(model)) return "claude-sonnet-4";
7414
- if (model === "claude-opus-4-8-1m") return "claude-opus-4.8";
7415
- if (model === "claude-opus-4-8") return "claude-opus-4.8";
7416
- if (model === "claude-opus-4-7-1m") return "claude-opus-4.7";
7417
- if (/^claude-opus-4-7$/.test(model)) return "claude-opus-4.7";
7418
- if (model === "claude-opus-4-6-1m") return "claude-opus-4.6-1m";
7419
- if (/^claude-opus-4-6$/.test(model)) return "claude-opus-4.6";
7420
- if (/^claude-opus-4-5-\d+$/.test(model)) return "claude-opus-4.5";
7421
- if (/^claude-opus-4-\d+$/.test(model)) return findLatestModel("claude-opus", "claude-opus-4.5");
7422
- if (/^claude-haiku-4-5-\d+$/.test(model)) return "claude-haiku-4.5";
7423
- if (/^claude-haiku-3-5-\d+$/.test(model)) return findLatestModel("claude-haiku", "claude-haiku-4.5");
7424
- return model;
7425
- }
7426
- function translateAnthropicMessagesToOpenAI(anthropicMessages, system, toolNameMapping) {
7427
- const systemMessages = handleSystemPrompt(system);
7428
- const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message, toolNameMapping));
7429
- return [...systemMessages, ...otherMessages];
7430
- }
7431
- const RESERVED_KEYWORDS = ["x-anthropic-billing-header", "x-anthropic-billing"];
7432
- /**
7433
- * Filter out reserved keywords from system prompt text.
7434
- * Copilot API rejects requests containing these keywords.
7435
- * Removes the entire line containing the keyword to keep the prompt clean.
7436
- */
7437
- function filterReservedKeywords(text) {
7438
- let filtered = text;
7439
- for (const keyword of RESERVED_KEYWORDS) if (text.includes(keyword)) {
7440
- consola.debug(`[Reserved Keyword] Removing line containing "${keyword}"`);
7441
- filtered = filtered.split("\n").filter((line) => !line.includes(keyword)).join("\n");
7442
- }
7443
- return filtered;
7444
- }
7445
- function handleSystemPrompt(system) {
7446
- if (!system) return [];
7447
- if (typeof system === "string") return [{
7448
- role: "system",
7449
- content: filterReservedKeywords(system)
7450
- }];
7451
- else return [{
7452
- role: "system",
7453
- content: filterReservedKeywords(system.map((block) => block.text).join("\n\n"))
7454
- }];
7455
- }
7456
- function handleUserMessage(message) {
7457
- const newMessages = [];
7458
- if (Array.isArray(message.content)) {
7459
- const toolResultBlocks = message.content.filter((block) => block.type === "tool_result");
7460
- const otherBlocks = message.content.filter((block) => block.type !== "tool_result");
7461
- for (const block of toolResultBlocks) newMessages.push({
7462
- role: "tool",
7463
- tool_call_id: block.tool_use_id,
7464
- content: mapContent(block.content)
7465
- });
7466
- if (otherBlocks.length > 0) newMessages.push({
7467
- role: "user",
7468
- content: mapContent(otherBlocks)
7469
- });
7470
- } else newMessages.push({
7471
- role: "user",
7472
- content: mapContent(message.content)
7473
- });
7474
- return newMessages;
7475
- }
7476
- function handleAssistantMessage(message, toolNameMapping) {
7477
- if (!Array.isArray(message.content)) return [{
7478
- role: "assistant",
7479
- content: mapContent(message.content)
7480
- }];
7481
- const toolUseBlocks = message.content.filter((block) => block.type === "tool_use");
7482
- const textBlocks = message.content.filter((block) => block.type === "text");
7483
- const thinkingBlocks = message.content.filter((block) => block.type === "thinking");
7484
- const allTextContent = [...textBlocks.map((b) => b.text), ...thinkingBlocks.map((b) => b.thinking)].join("\n\n");
7485
- return toolUseBlocks.length > 0 ? [{
7486
- role: "assistant",
7487
- content: allTextContent || null,
7488
- tool_calls: toolUseBlocks.map((toolUse) => ({
7489
- id: toolUse.id,
7490
- type: "function",
7491
- function: {
7492
- name: getTruncatedToolName(toolUse.name, toolNameMapping),
7493
- arguments: JSON.stringify(toolUse.input)
7494
- }
7495
- }))
7496
- }] : [{
7497
- role: "assistant",
7498
- content: mapContent(message.content)
7499
- }];
7500
- }
7501
- function mapContent(content) {
7502
- if (typeof content === "string") return content;
7503
- if (!Array.isArray(content)) return null;
7504
- if (!content.some((block) => block.type === "image")) return content.filter((block) => block.type === "text" || block.type === "thinking").map((block) => block.type === "text" ? block.text : block.thinking).join("\n\n");
7505
- const contentParts = [];
7506
- for (const block of content) switch (block.type) {
7507
- case "text":
7508
- contentParts.push({
7509
- type: "text",
7510
- text: block.text
7511
- });
7512
- break;
7513
- case "thinking":
7514
- contentParts.push({
7515
- type: "text",
7516
- text: block.thinking
7517
- });
7518
- break;
7519
- case "image":
7520
- contentParts.push({
7521
- type: "image_url",
7522
- image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
7523
- });
7524
- break;
7525
- }
7526
- return contentParts;
7527
- }
7528
- function getTruncatedToolName(originalName, toolNameMapping) {
7529
- if (originalName.length <= OPENAI_TOOL_NAME_LIMIT) return originalName;
7530
- const existingTruncated = toolNameMapping.originalToTruncated.get(originalName);
7531
- if (existingTruncated) return existingTruncated;
7532
- let hash = 0;
7533
- for (let i = 0; i < originalName.length; i++) {
7534
- const char = originalName.codePointAt(i) ?? 0;
7535
- hash = (hash << 5) - hash + char;
7536
- hash = Math.trunc(hash);
7537
- }
7538
- const hashSuffix = Math.abs(hash).toString(36).slice(0, 8);
7539
- const truncatedName = originalName.slice(0, OPENAI_TOOL_NAME_LIMIT - 9) + "_" + hashSuffix;
7540
- toolNameMapping.truncatedToOriginal.set(truncatedName, originalName);
7541
- toolNameMapping.originalToTruncated.set(originalName, truncatedName);
7542
- consola.debug(`Truncated tool name: "${originalName}" -> "${truncatedName}"`);
7543
- return truncatedName;
7544
- }
7545
- function translateAnthropicToolsToOpenAI(anthropicTools, toolNameMapping) {
7546
- if (!anthropicTools) return;
7547
- return anthropicTools.map((tool) => ({
7548
- type: "function",
7549
- function: {
7550
- name: getTruncatedToolName(tool.name, toolNameMapping),
7551
- description: tool.description,
7552
- parameters: tool.input_schema ?? {}
7553
- }
7554
- }));
7555
- }
7556
- function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice, toolNameMapping) {
7557
- if (!anthropicToolChoice) return;
7558
- switch (anthropicToolChoice.type) {
7559
- case "auto": return "auto";
7560
- case "any": return "required";
7561
- case "tool":
7562
- if (anthropicToolChoice.name) return {
7563
- type: "function",
7564
- function: { name: getTruncatedToolName(anthropicToolChoice.name, toolNameMapping) }
7565
- };
7566
- return;
7567
- case "none": return "none";
7568
- default: return;
7569
- }
7570
- }
7571
- /** Create empty response for edge case of no choices */
7572
- function createEmptyResponse(response) {
7573
- return {
7574
- id: response.id,
7575
- type: "message",
7576
- role: "assistant",
7577
- model: response.model,
7578
- content: [],
7579
- stop_reason: "end_turn",
7580
- stop_sequence: null,
7581
- usage: {
7582
- input_tokens: response.usage?.prompt_tokens ?? 0,
7583
- output_tokens: response.usage?.completion_tokens ?? 0
7584
- }
7585
- };
7586
- }
7587
- /** Build usage object from response */
7588
- function buildUsageObject(response) {
7589
- const cachedTokens = response.usage?.prompt_tokens_details?.cached_tokens;
7590
- return {
7591
- input_tokens: (response.usage?.prompt_tokens ?? 0) - (cachedTokens ?? 0),
7592
- output_tokens: response.usage?.completion_tokens ?? 0,
7593
- ...cachedTokens !== void 0 && { cache_read_input_tokens: cachedTokens }
7594
- };
7595
- }
7596
- function translateToAnthropic(response, toolNameMapping) {
7597
- if (response.choices.length === 0) return createEmptyResponse(response);
7598
- const allTextBlocks = [];
7599
- const allToolUseBlocks = [];
7600
- let stopReason = null;
7601
- stopReason = response.choices[0]?.finish_reason ?? stopReason;
7602
- for (const choice of response.choices) {
7603
- const textBlocks = getAnthropicTextBlocks(choice.message.content);
7604
- const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls, toolNameMapping);
7605
- allTextBlocks.push(...textBlocks);
7606
- allToolUseBlocks.push(...toolUseBlocks);
7607
- if (choice.finish_reason === "tool_calls" || stopReason === "stop") stopReason = choice.finish_reason;
7608
- }
7609
- return {
7610
- id: response.id,
7611
- type: "message",
7612
- role: "assistant",
7613
- model: response.model,
7614
- content: [...allTextBlocks, ...allToolUseBlocks],
7615
- stop_reason: mapOpenAIStopReasonToAnthropic(stopReason),
7616
- stop_sequence: null,
7617
- usage: buildUsageObject(response)
7618
- };
7619
- }
7620
- function getAnthropicTextBlocks(messageContent) {
7621
- if (typeof messageContent === "string") return [{
7622
- type: "text",
7623
- text: messageContent
7624
- }];
7625
- if (Array.isArray(messageContent)) return messageContent.filter((part) => part.type === "text").map((part) => ({
7626
- type: "text",
7627
- text: part.text
7628
- }));
7629
- return [];
7630
- }
7631
- function getAnthropicToolUseBlocks(toolCalls, toolNameMapping) {
7632
- if (!toolCalls) return [];
7633
- return toolCalls.map((toolCall) => {
7634
- let input = {};
7635
- try {
7636
- input = JSON.parse(toolCall.function.arguments);
7637
- } catch (error) {
7638
- consola.warn(`Failed to parse tool call arguments for ${toolCall.function.name}:`, error);
7639
- }
7640
- const originalName = toolNameMapping?.truncatedToOriginal.get(toolCall.function.name) ?? toolCall.function.name;
7641
- return {
7642
- type: "tool_use",
7643
- id: toolCall.id,
7644
- name: originalName,
7645
- input
7646
- };
7647
- });
7648
- }
7649
-
7650
- //#endregion
7651
- //#region src/routes/messages/stream-translation.ts
7652
- function isToolBlockOpen(state) {
7653
- if (!state.contentBlockOpen) return false;
7654
- return Object.values(state.toolCalls).some((tc) => tc.anthropicBlockIndex === state.contentBlockIndex);
7655
- }
7656
- function translateChunkToAnthropicEvents(chunk, state, toolNameMapping) {
7657
- const events = [];
7658
- if (chunk.choices.length === 0) {
7659
- if (chunk.model && !state.model) state.model = chunk.model;
7660
- return events;
7661
- }
7662
- const choice = chunk.choices[0];
7663
- const { delta } = choice;
7664
- if (!state.messageStartSent) {
7665
- const model = chunk.model || state.model || "unknown";
7666
- events.push({
7667
- type: "message_start",
7668
- message: {
7669
- id: chunk.id || `msg_${Date.now()}`,
7670
- type: "message",
7671
- role: "assistant",
7672
- content: [],
7673
- model,
7674
- stop_reason: null,
7675
- stop_sequence: null,
7676
- usage: {
7677
- input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
7678
- output_tokens: 0,
7679
- ...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
7680
- }
7681
- }
7682
- });
7683
- state.messageStartSent = true;
7684
- }
7685
- if (delta.content) {
7686
- if (isToolBlockOpen(state)) {
7687
- events.push({
7688
- type: "content_block_stop",
7689
- index: state.contentBlockIndex
7690
- });
7691
- state.contentBlockIndex++;
7692
- state.contentBlockOpen = false;
7693
- }
7694
- if (!state.contentBlockOpen) {
7695
- events.push({
7696
- type: "content_block_start",
7697
- index: state.contentBlockIndex,
7698
- content_block: {
7699
- type: "text",
7700
- text: ""
7701
- }
7702
- });
7703
- state.contentBlockOpen = true;
7704
- }
7705
- events.push({
7706
- type: "content_block_delta",
7707
- index: state.contentBlockIndex,
7708
- delta: {
7709
- type: "text_delta",
7710
- text: delta.content
7711
- }
7712
- });
7713
- }
7714
- if (delta.tool_calls) for (const toolCall of delta.tool_calls) {
7715
- if (toolCall.id && toolCall.function?.name) {
7716
- if (state.contentBlockOpen) {
7717
- events.push({
7718
- type: "content_block_stop",
7719
- index: state.contentBlockIndex
7720
- });
7721
- state.contentBlockIndex++;
7722
- state.contentBlockOpen = false;
7723
- }
7724
- const originalName = toolNameMapping?.truncatedToOriginal.get(toolCall.function.name) ?? toolCall.function.name;
7725
- const anthropicBlockIndex = state.contentBlockIndex;
7726
- state.toolCalls[toolCall.index] = {
7727
- id: toolCall.id,
7728
- name: originalName,
7729
- anthropicBlockIndex
7730
- };
7731
- events.push({
7732
- type: "content_block_start",
7733
- index: anthropicBlockIndex,
7734
- content_block: {
7735
- type: "tool_use",
7736
- id: toolCall.id,
7737
- name: originalName,
7738
- input: {}
7739
- }
7740
- });
7741
- state.contentBlockOpen = true;
7742
- }
7743
- if (toolCall.function?.arguments) {
7744
- const toolCallInfo = state.toolCalls[toolCall.index];
7745
- if (toolCallInfo) events.push({
7746
- type: "content_block_delta",
7747
- index: toolCallInfo.anthropicBlockIndex,
7748
- delta: {
7749
- type: "input_json_delta",
7750
- partial_json: toolCall.function.arguments
7751
- }
7752
- });
7753
- }
7754
- }
7755
- if (choice.finish_reason) {
7756
- if (state.contentBlockOpen) {
7757
- events.push({
7758
- type: "content_block_stop",
7759
- index: state.contentBlockIndex
7760
- });
7761
- state.contentBlockOpen = false;
7762
- }
7763
- events.push({
7764
- type: "message_delta",
7765
- delta: {
7766
- stop_reason: mapOpenAIStopReasonToAnthropic(choice.finish_reason),
7767
- stop_sequence: null
7768
- },
7769
- usage: {
7770
- input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
7771
- output_tokens: chunk.usage?.completion_tokens ?? 0,
7772
- ...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
7773
- }
7774
- }, { type: "message_stop" });
7775
- }
7776
- return events;
7777
- }
7778
- function translateErrorToAnthropicErrorEvent(error) {
7779
- return {
7780
- type: "error",
7781
- error: {
7782
- type: "api_error",
7783
- message: error ? formatError(error) : "An unexpected error occurred during streaming."
7784
- }
7785
- };
7786
- }
7787
-
7788
- //#endregion
7789
- //#region src/routes/messages/tool-call-recovery.ts
7790
- const ENVELOPE = String.raw`(?:<(?:antml:)?function_calls>|call|count|court)`;
7791
- const INVOKE_BODY = String.raw`<(?:antml:)?invoke\s+name="[^"]+">(?:(?!<(?:antml:)?invoke\b)[\s\S])*?</(?:antml:)?invoke>`;
7792
- const LEAKED_REGION_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*(?:` + INVOKE_BODY + String.raw`\s*)+(?:</(?:antml:)?function_calls>)?|` + INVOKE_BODY + String.raw`)`, "g");
7793
- const STARTS_WITH_ENVELOPE_RE = new RegExp(String.raw`^[ \t\n]*` + ENVELOPE);
7794
- const INVOKE_OPENER_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*)?<(?:antml:)?invoke\b[^\n>]*>?`, "g");
7795
- const LEAK_OPEN_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*)?<(?:antml:)?invoke\s+name="`, "g");
7796
- const INVOKE_RE = /<(?:antml:)?invoke\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?invoke>/g;
7797
- const PARAMETER_RE = /<(?:antml:)?parameter\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?parameter>/g;
7798
- function coerceParamValue(raw) {
7799
- const trimmed = raw.trim();
7800
- if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) try {
7801
- return JSON.parse(trimmed);
7802
- } catch {
7803
- return raw;
7804
- }
7805
- return raw;
7806
- }
7807
- function parseRegionInvokes(region, knownTools) {
7808
- const calls = [];
7809
- for (const invokeMatch of region.matchAll(INVOKE_RE)) {
7810
- const name = invokeMatch[1];
7811
- if (knownTools && !knownTools.has(name)) continue;
7812
- const input = {};
7813
- for (const paramMatch of invokeMatch[2].matchAll(PARAMETER_RE)) input[paramMatch[1]] = coerceParamValue(paramMatch[2]);
7814
- calls.push({
7815
- name,
7816
- input
7817
- });
7818
- }
7819
- return calls;
7820
- }
7821
- const FENCE_DELIM_RE = /(?:^|\n)[ \t]*```/g;
7822
- function insideFence(text, pos) {
7823
- let openAt = -1;
7824
- for (const m of text.matchAll(FENCE_DELIM_RE)) if (openAt === -1) {
7825
- if (m.index >= pos) break;
7826
- openAt = m.index;
7827
- } else if (m.index > pos) return true;
7828
- else openAt = -1;
7829
- return false;
7830
- }
7831
- function regionIsLeak(region, offset, fullText, knownTools) {
7832
- if (insideFence(fullText, offset)) return false;
7833
- if (STARTS_WITH_ENVELOPE_RE.test(region)) return true;
7834
- if (!knownTools) return false;
7835
- return parseRegionInvokes(region, knownTools).length > 0;
7836
- }
7837
- /**
7838
- * Split assistant text into ordered segments — dropping leaked envelope markup
7839
- * (and undeclared-tool invokes) while preserving the natural-language on either
7840
- * side and the pre/tool/post ordering. Returns a single text segment when there
7841
- * is no leak. Shared by both response recovery paths so they cannot diverge.
7338
+ * Split assistant text into ordered segments dropping leaked envelope markup
7339
+ * (and undeclared-tool invokes) while preserving the natural-language on either
7340
+ * side and the pre/tool/post ordering. Returns a single text segment when there
7341
+ * is no leak. Shared by both response recovery paths so they cannot diverge.
7842
7342
  *
7843
7343
  * `from` restricts the EMITTED window to `text.slice(from)` while still
7844
7344
  * classifying against the FULL `text` — so a streaming capture that started just
@@ -8643,359 +8143,350 @@ const parseSubagentMarkerFromSystemReminder = (text) => {
8643
8143
  };
8644
8144
 
8645
8145
  //#endregion
8646
- //#region src/routes/messages/translated-handler.ts
8146
+ //#region src/routes/messages/handler.ts
8147
+ function resolveModelFromBetaHeader(model, betaHeader) {
8148
+ if (!betaHeader || !/\bcontext-1m\b/.test(betaHeader)) return model;
8149
+ if (!model.startsWith("claude-")) return model;
8150
+ if (model.endsWith("-1m")) return model;
8151
+ const resolved = `${model}-1m`;
8152
+ consola.debug(`Detected context-1m in anthropic-beta header, resolving model: ${model} → ${resolved}`);
8153
+ return resolved;
8154
+ }
8155
+ async function handleCompletion(c) {
8156
+ const rawPayload = await c.req.json();
8157
+ consola.debug("Anthropic request payload:", JSON.stringify(rawPayload));
8158
+ if (rawPayload === null || typeof rawPayload.model !== "string" || rawPayload.model.length === 0) return c.json({
8159
+ type: "error",
8160
+ error: {
8161
+ type: "invalid_request_error",
8162
+ message: "model is required and must be a non-empty string"
8163
+ }
8164
+ }, 400);
8165
+ const normalizedModel = resolveModelFromBetaHeader(rawPayload.model, c.req.header("anthropic-beta"));
8166
+ if (!supportsDirectAnthropicApi(normalizedModel)) return c.json({
8167
+ type: "error",
8168
+ error: {
8169
+ type: "invalid_request_error",
8170
+ message: `model \`${normalizedModel}\` is not an Anthropic model available on /v1/messages`
8171
+ }
8172
+ }, 400);
8173
+ const { ctx, payload: anthropicPayload } = createEntryContext({
8174
+ c,
8175
+ rawPayload,
8176
+ endpoint: "anthropic",
8177
+ normalizePayload: (p) => ({
8178
+ ...p,
8179
+ model: resolveModelFromBetaHeader(p.model, c.req.header("anthropic-beta"))
8180
+ }),
8181
+ buildHistoryRequest: (p) => ({
8182
+ model: p.model,
8183
+ messages: convertAnthropicMessages(p.messages),
8184
+ stream: p.stream ?? false,
8185
+ tools: p.tools?.map((t) => ({
8186
+ name: t.name,
8187
+ description: t.description
8188
+ })),
8189
+ max_tokens: p.max_tokens,
8190
+ temperature: p.temperature,
8191
+ system: extractSystemPrompt(p.system)
8192
+ })
8193
+ });
8194
+ const sanitizedPayload = dePoisonAssistantMessages(anthropicPayload);
8195
+ logToolInfo(sanitizedPayload);
8196
+ const subagentMarker = parseSubagentMarkerFromFirstUser(sanitizedPayload);
8197
+ const initiatorOverride = subagentMarker ? "agent" : void 0;
8198
+ if (subagentMarker) consola.debug("Detected Subagent marker:", JSON.stringify(subagentMarker));
8199
+ normalizeSystemPromptDate(sanitizedPayload);
8200
+ injectSystemCacheControl(sanitizedPayload);
8201
+ return handleDirectAnthropicCompletion(c, sanitizedPayload, ctx, initiatorOverride);
8202
+ }
8647
8203
  /**
8648
- * Handle completion using OpenAI translation path (legacy)
8204
+ * Log tool-related information for debugging
8649
8205
  */
8650
- async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOverride) {
8651
- const originalModelId = anthropicPayload.model;
8652
- const hasOneMillionSuffix = isOneMillionSuffixedClaudeId(originalModelId);
8653
- const oneMResolution = resolveAnthropicModelForDirectPath(originalModelId);
8654
- const needsContext1mBeta = hasOneMillionSuffix || (oneMResolution?.oneMillionFallback ?? false);
8655
- const { payload: translatedPayload, toolNameMapping } = translateToOpenAI(anthropicPayload);
8656
- consola.debug("Translated OpenAI request payload:", JSON.stringify(translatedPayload));
8657
- updateTrackerResolvedModel(ctx.trackingId, translatedPayload.model);
8658
- const selectedModel = findModelById(translatedPayload.model);
8659
- const autoTruncateConfig = oneMResolution?.oneMillionFallback ? {
8660
- contextWindowOverride: oneMResolution.effectiveContextWindowTokens,
8661
- tokenLimitCacheKeyOverride: originalModelId
8662
- } : {};
8663
- const { finalPayload: openAIPayload, truncateResult } = await buildFinalPayload(translatedPayload, selectedModel, autoTruncateConfig);
8664
- if (truncateResult) ctx.truncateResult = truncateResult;
8665
- let anthropicBeta = c.req.header("anthropic-beta");
8666
- if (needsContext1mBeta) anthropicBeta = appendContext1mBeta(anthropicBeta);
8667
- if (state.manualApprove) await awaitApproval();
8668
- let errorModelIdOverride;
8669
- if (autoTruncateConfig.tokenLimitCacheKeyOverride !== void 0) errorModelIdOverride = autoTruncateConfig.tokenLimitCacheKeyOverride;
8670
- else if (needsContext1mBeta && selectedModel) errorModelIdOverride = selectedModel.id;
8671
- else if (hasOneMillionSuffix) errorModelIdOverride = originalModelId;
8672
- const abort = clientAbortController(c);
8673
- const isStreaming = anthropicPayload.stream === true;
8674
- try {
8675
- const settled = settle(executeWithAdaptiveRateLimit(() => createChatCompletions(openAIPayload, {
8676
- initiator: initiatorOverride,
8677
- resolvedModel: selectedModel,
8678
- anthropicBeta,
8679
- errorModelIdOverride,
8680
- signal: abort.signal
8681
- })));
8682
- const raced = isStreaming ? await raceWithGrace(settled, RATE_LIMIT_GRACE_MS) : await settled;
8683
- if (raced.kind === "error") throw raced.error;
8684
- if (raced.kind === "done") {
8685
- const { result: response, queueWaitMs } = raced.value;
8686
- ctx.queueWaitMs = queueWaitMs;
8687
- if (isNonStreaming(response)) return handleNonStreamingResponse({
8688
- c,
8689
- response,
8690
- toolNameMapping,
8691
- ctx,
8692
- anthropicPayload
8693
- });
8694
- consola.debug("Streaming response from Copilot");
8695
- updateTrackerStatus(ctx.trackingId, "streaming");
8696
- return streamSSE(c, async (stream) => {
8697
- stream.onAbort(() => abort.abort());
8698
- await handleStreamingResponse({
8699
- stream,
8700
- response,
8701
- toolNameMapping,
8702
- anthropicPayload,
8703
- ctx
8206
+ function logToolInfo(anthropicPayload) {
8207
+ if (anthropicPayload.tools?.length) {
8208
+ const toolInfo = anthropicPayload.tools.map((t) => ({
8209
+ name: t.name,
8210
+ type: t.type ?? "(custom)"
8211
+ }));
8212
+ consola.debug(`[Tools] Defined tools:`, JSON.stringify(toolInfo));
8213
+ }
8214
+ for (const msg of anthropicPayload.messages) if (typeof msg.content !== "string") for (const block of msg.content) {
8215
+ if (block.type === "tool_use") consola.debug(`[Tools] tool_use in message: ${block.name} (id: ${block.id})`);
8216
+ if (block.type === "tool_result") consola.debug(`[Tools] tool_result in message: id=${block.tool_use_id}, is_error=${block.is_error ?? false}`);
8217
+ }
8218
+ }
8219
+
8220
+ //#endregion
8221
+ //#region src/routes/messages/non-stream-translation.ts
8222
+ const OPENAI_TOOL_NAME_LIMIT = 64;
8223
+ /**
8224
+ * Ensure all tool_use blocks have corresponding tool_result responses.
8225
+ * This handles edge cases where conversation history may be incomplete:
8226
+ * - Session interruptions where tool execution was cut off
8227
+ * - Previous request failures
8228
+ * - Client sending truncated history
8229
+ *
8230
+ * Adding placeholder responses prevents API errors and maintains protocol compliance.
8231
+ */
8232
+ function fixMessageSequence(messages) {
8233
+ const fixedMessages = [];
8234
+ for (let i = 0; i < messages.length; i++) {
8235
+ const message = messages[i];
8236
+ fixedMessages.push(message);
8237
+ if (message.role === "assistant" && message.tool_calls && message.tool_calls.length > 0) {
8238
+ const foundToolResponses = /* @__PURE__ */ new Set();
8239
+ let j = i + 1;
8240
+ while (j < messages.length && messages[j].role === "tool") {
8241
+ const toolMessage = messages[j];
8242
+ if (toolMessage.tool_call_id) foundToolResponses.add(toolMessage.tool_call_id);
8243
+ j++;
8244
+ }
8245
+ for (const toolCall of message.tool_calls) if (!foundToolResponses.has(toolCall.id)) {
8246
+ consola.debug(`Adding placeholder tool_result for ${toolCall.id}`);
8247
+ fixedMessages.push({
8248
+ role: "tool",
8249
+ tool_call_id: toolCall.id,
8250
+ content: "Tool execution was interrupted or failed."
8704
8251
  });
8705
- });
8706
- }
8707
- consola.debug("[RateLimiter] Request queued past grace; opening keepalive stream (translated)");
8708
- updateTrackerStatus(ctx.trackingId, "streaming");
8709
- return streamSSE(c, async (stream) => {
8710
- stream.onAbort(() => abort.abort());
8711
- await runStreamWithKeepalive({
8712
- stream,
8713
- settled,
8714
- pingIntervalMs: KEEPALIVE_PING_INTERVAL_MS,
8715
- onResponse: async ({ result: response, queueWaitMs }) => {
8716
- ctx.queueWaitMs = queueWaitMs;
8717
- if (isNonStreaming(response)) return;
8718
- await handleStreamingResponse({
8719
- stream,
8720
- response,
8721
- toolNameMapping,
8722
- anthropicPayload,
8723
- ctx
8724
- });
8725
- },
8726
- onError: async (error) => {
8727
- if (isAbortError(error)) {
8728
- consola.debug("[Translated] client disconnected during keepalive; upstream aborted");
8729
- failTracking(ctx.trackingId, "client disconnected");
8730
- return;
8731
- }
8732
- recordStreamError({
8733
- acc: createAnthropicStreamAccumulator(),
8734
- fallbackModel: anthropicPayload.model,
8735
- ctx,
8736
- error,
8737
- endpoint: "messages"
8738
- });
8739
- failTracking(ctx.trackingId, error);
8740
- const errorEvent = translateErrorToAnthropicErrorEvent(error);
8741
- await stream.writeSSE({
8742
- event: errorEvent.type,
8743
- data: JSON.stringify(errorEvent)
8744
- });
8745
- }
8746
- });
8747
- });
8748
- } catch (error) {
8749
- if (isAbortError(error)) {
8750
- consola.debug("[Translated] client disconnected before response; upstream aborted");
8751
- failTracking(ctx.trackingId, "client disconnected");
8752
- return new Response(null, { status: 499 });
8252
+ }
8753
8253
  }
8754
- if (error instanceof HTTPError && error.status === 413) await logPayloadSizeInfo(openAIPayload, selectedModel);
8755
- recordErrorResponse(ctx, anthropicPayload.model, error, "messages", anthropicPayload.stream ?? false);
8756
- throw error;
8757
8254
  }
8255
+ return fixedMessages;
8758
8256
  }
8759
- function handleNonStreamingResponse(opts) {
8760
- const { c, response, toolNameMapping, ctx, anthropicPayload } = opts;
8761
- consola.debug("Non-streaming response from Copilot:", JSON.stringify(response).slice(-400));
8762
- let anthropicResponse = translateToAnthropic(response, toolNameMapping);
8763
- consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
8764
- if (state.verbose && ctx.truncateResult?.wasCompacted) {
8765
- const marker = formatClientTruncationMarker(ctx.truncateResult);
8766
- anthropicResponse = prependMarkerToResponse(anthropicResponse, marker);
8767
- }
8768
- recordResponse(ctx.historyId, {
8769
- success: true,
8770
- model: anthropicResponse.model,
8771
- usage: anthropicResponse.usage,
8772
- stop_reason: anthropicResponse.stop_reason ?? void 0,
8773
- content: {
8774
- role: "assistant",
8775
- content: anthropicResponse.content.map((block) => {
8776
- if (block.type === "text") return {
8777
- type: "text",
8778
- text: block.text
8779
- };
8780
- if (block.type === "tool_use") return {
8781
- type: "tool_use",
8782
- id: block.id,
8783
- name: block.name,
8784
- input: JSON.stringify(block.input)
8785
- };
8786
- return { type: block.type };
8787
- })
8788
- },
8789
- toolCalls: extractToolCallsFromContent(anthropicResponse.content)
8790
- }, Date.now() - ctx.startTime);
8791
- const cacheRead = anthropicResponse.usage.cache_read_input_tokens ?? 0;
8792
- const cacheCreation = anthropicResponse.usage.cache_creation_input_tokens ?? 0;
8793
- const totalInputTokens = anthropicResponse.usage.input_tokens + cacheRead + cacheCreation;
8794
- if (ctx.trackingId) requestTracker.updateRequest(ctx.trackingId, {
8795
- inputTokens: anthropicResponse.usage.input_tokens,
8796
- outputTokens: anthropicResponse.usage.output_tokens,
8797
- queueWaitMs: ctx.queueWaitMs,
8798
- cachedInputTokens: cacheRead,
8799
- cacheCreationInputTokens: cacheCreation,
8800
- totalInputTokens
8801
- });
8802
- captureRequest({
8803
- model: anthropicResponse.model,
8804
- inputTokens: anthropicResponse.usage.input_tokens,
8805
- outputTokens: anthropicResponse.usage.output_tokens,
8806
- durationMs: Date.now() - ctx.startTime,
8807
- success: true,
8808
- stream: false,
8809
- toolCount: anthropicPayload.tools?.length ?? 0,
8810
- cachedInputTokens: cacheRead,
8811
- cacheCreationInputTokens: cacheCreation,
8812
- totalInputTokens,
8813
- stopReason: anthropicResponse.stop_reason ?? void 0
8257
+ function translateToOpenAI(payload) {
8258
+ const toolNameMapping = { originalToTruncated: /* @__PURE__ */ new Map() };
8259
+ const messages = translateAnthropicMessagesToOpenAI(payload.messages, payload.system, toolNameMapping);
8260
+ return { payload: {
8261
+ model: translateModelName(payload.model),
8262
+ messages: fixMessageSequence(messages),
8263
+ max_tokens: payload.max_tokens,
8264
+ stop: payload.stop_sequences,
8265
+ stream: payload.stream,
8266
+ temperature: payload.temperature,
8267
+ top_p: payload.top_p,
8268
+ user: payload.metadata?.user_id,
8269
+ tools: translateAnthropicToolsToOpenAI(payload.tools, toolNameMapping),
8270
+ tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice, toolNameMapping)
8271
+ } };
8272
+ }
8273
+ /**
8274
+ * Find the latest available model matching a family prefix.
8275
+ * Searches state.models for models starting with the given prefix
8276
+ * and returns the one with the highest version number.
8277
+ *
8278
+ * @param familyPrefix - e.g., "claude-opus", "claude-sonnet", "claude-haiku"
8279
+ * @param fallback - fallback model ID if no match found
8280
+ */
8281
+ function findLatestModel(familyPrefix, fallback) {
8282
+ const models = state.models?.data;
8283
+ if (!models || models.length === 0) return fallback;
8284
+ const candidates = models.filter((m) => m.id.startsWith(familyPrefix));
8285
+ if (candidates.length === 0) return fallback;
8286
+ candidates.sort((a, b) => {
8287
+ const [aMajor, aMinor] = extractVersion(a.id, familyPrefix);
8288
+ const [bMajor, bMinor] = extractVersion(b.id, familyPrefix);
8289
+ if (aMajor !== bMajor) return bMajor - aMajor;
8290
+ return bMinor - aMinor;
8814
8291
  });
8815
- return c.json(echoResponseBody(anthropicResponse, ctx));
8292
+ return candidates[0].id;
8816
8293
  }
8817
- async function handleStreamingResponse(opts) {
8818
- const { stream, response, toolNameMapping, anthropicPayload, ctx } = opts;
8819
- const streamState = {
8820
- messageStartSent: false,
8821
- contentBlockIndex: 0,
8822
- contentBlockOpen: false,
8823
- toolCalls: {}
8824
- };
8825
- const acc = createAnthropicStreamAccumulator();
8826
- const checkRepetition = createStreamRepetitionChecker(`translated:${anthropicPayload.model}`);
8827
- try {
8828
- if (ctx.truncateResult?.wasCompacted) {
8829
- const marker = formatClientTruncationMarker(ctx.truncateResult);
8830
- await sendTruncationMarkerEvent(stream, streamState, marker);
8831
- acc.content += marker;
8832
- }
8833
- await processStreamChunks({
8834
- stream,
8835
- response,
8836
- toolNameMapping,
8837
- streamState,
8838
- acc,
8839
- checkRepetition,
8840
- ctx
8841
- });
8842
- recordAnthropicStreamingResponse(acc, anthropicPayload.model, ctx);
8843
- completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens, ctx.queueWaitMs, void 0, {
8844
- model: acc.model || anthropicPayload.model,
8845
- stream: true,
8846
- durationMs: Date.now() - ctx.startTime,
8847
- stopReason: acc.stopReason || void 0,
8848
- toolCount: anthropicPayload.tools?.length ?? 0
8849
- }, ctx.timings, {
8850
- cachedInputTokens: acc.cacheReadInputTokens,
8851
- cacheCreationInputTokens: acc.cacheCreationInputTokens,
8852
- totalInputTokens: acc.inputTokens + acc.cacheReadInputTokens + acc.cacheCreationInputTokens
8853
- });
8854
- } catch (error) {
8855
- if (isAbortError(error)) {
8856
- consola.debug("[Translated] client disconnected mid-stream; upstream aborted");
8857
- failTracking(ctx.trackingId, "client disconnected");
8858
- return;
8859
- }
8860
- consola.error("Stream error:", formatError(error));
8861
- recordStreamError({
8862
- acc,
8863
- fallbackModel: anthropicPayload.model,
8864
- ctx,
8865
- error,
8866
- endpoint: "messages"
8867
- });
8868
- failTracking(ctx.trackingId, error);
8869
- const errorEvent = translateErrorToAnthropicErrorEvent(error);
8870
- await stream.writeSSE({
8871
- event: errorEvent.type,
8872
- data: JSON.stringify(errorEvent)
8873
- });
8874
- }
8294
+ /**
8295
+ * Extract numeric [major, minor] version from a model id.
8296
+ *
8297
+ * Supports both naming conventions Anthropic/Copilot have used:
8298
+ * - dot: "claude-opus-4.5" → [4, 5]
8299
+ * - dash: "claude-opus-4-8" → [4, 8]
8300
+ * - dash double-digit: "claude-opus-4-10" → [4, 10]
8301
+ *
8302
+ * The dash form previously parsed as just the major via the regex
8303
+ * /^(\d+(?:\.\d+)?)/ (the dash stopped the match), which silently
8304
+ * downgraded dash-named candidates against any dot-named candidate in
8305
+ * findLatestModel. Parsing into a tuple also avoids the parseFloat
8306
+ * lossiness on double-digit minors ("4.10" → 4.1).
8307
+ *
8308
+ * Anything after the major/minor segment (date stamps, "-1m") is ignored.
8309
+ * The minor capture is bounded to 1-3 digits so that an 8-digit date suffix
8310
+ * directly after the major (e.g. "claude-opus-4-20250514") is NOT mistaken
8311
+ * for a minor version of 20_250_514 — without that bound, dated ids would
8312
+ * outrank legitimate dotted candidates like "claude-opus-4.8" in
8313
+ * findLatestModel's sort.
8314
+ *
8315
+ * Returns [0, 0] when no version can be extracted.
8316
+ */
8317
+ function extractVersion(modelId, prefix) {
8318
+ const match = modelId.slice(prefix.length + 1).match(/^(\d+)(?:[.-](\d{1,3}))?/);
8319
+ if (!match) return [0, 0];
8320
+ const major = Number.parseInt(match[1], 10);
8321
+ const rawMinor = match[2];
8322
+ return [major, rawMinor === void 0 ? 0 : Number.parseInt(rawMinor, 10) || 0];
8875
8323
  }
8876
- async function sendTruncationMarkerEvent(stream, streamState, marker) {
8877
- const blockStartEvent = {
8878
- type: "content_block_start",
8879
- index: streamState.contentBlockIndex,
8880
- content_block: {
8881
- type: "text",
8882
- text: ""
8883
- }
8884
- };
8885
- await stream.writeSSE({
8886
- event: "content_block_start",
8887
- data: JSON.stringify(blockStartEvent)
8888
- });
8889
- const deltaEvent = {
8890
- type: "content_block_delta",
8891
- index: streamState.contentBlockIndex,
8892
- delta: {
8893
- type: "text_delta",
8894
- text: marker
8895
- }
8896
- };
8897
- await stream.writeSSE({
8898
- event: "content_block_delta",
8899
- data: JSON.stringify(deltaEvent)
8900
- });
8901
- const blockStopEvent = {
8902
- type: "content_block_stop",
8903
- index: streamState.contentBlockIndex
8324
+ function translateModelName(model) {
8325
+ const aliasMap = {
8326
+ opus: "claude-opus",
8327
+ sonnet: "claude-sonnet",
8328
+ haiku: "claude-haiku"
8904
8329
  };
8905
- await stream.writeSSE({
8906
- event: "content_block_stop",
8907
- data: JSON.stringify(blockStopEvent)
8908
- });
8909
- streamState.contentBlockIndex++;
8910
- }
8911
- async function processStreamChunks(opts) {
8912
- const { stream, response, toolNameMapping, streamState, acc, checkRepetition, ctx } = opts;
8913
- for await (const rawEvent of response) {
8914
- consola.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
8915
- if (rawEvent.data === "[DONE]") break;
8916
- if (!rawEvent.data) continue;
8917
- let chunk;
8918
- try {
8919
- chunk = JSON.parse(rawEvent.data);
8920
- } catch (parseError) {
8921
- consola.error("Failed to parse stream chunk:", parseError, rawEvent.data);
8922
- continue;
8923
- }
8924
- if (chunk.model && !acc.model) acc.model = chunk.model;
8925
- const events = translateChunkToAnthropicEvents(chunk, streamState, toolNameMapping);
8926
- for (const event of events) {
8927
- consola.debug("Translated Anthropic event:", JSON.stringify(event));
8928
- processAnthropicEvent(event, acc);
8929
- if (event.type === "content_block_delta" && event.delta.type === "text_delta") checkRepetition(event.delta.text);
8930
- const echoed = echoParsedEvent(event, ctx);
8931
- await stream.writeSSE({
8932
- event: echoed.type,
8933
- data: JSON.stringify(echoed)
8934
- });
8935
- }
8330
+ if (aliasMap[model]) {
8331
+ const familyPrefix = aliasMap[model];
8332
+ return findLatestModel(familyPrefix, `${familyPrefix}-4.5`);
8936
8333
  }
8334
+ if (/^claude-sonnet-4-5-\d+$/.test(model)) return "claude-sonnet-4.5";
8335
+ if (/^claude-sonnet-4-\d+$/.test(model)) return "claude-sonnet-4";
8336
+ if (model === "claude-opus-4-8-1m") return "claude-opus-4.8";
8337
+ if (model === "claude-opus-4-8") return "claude-opus-4.8";
8338
+ if (model === "claude-opus-4-7-1m") return "claude-opus-4.7";
8339
+ if (/^claude-opus-4-7$/.test(model)) return "claude-opus-4.7";
8340
+ if (model === "claude-opus-4-6-1m") return "claude-opus-4.6-1m";
8341
+ if (/^claude-opus-4-6$/.test(model)) return "claude-opus-4.6";
8342
+ if (/^claude-opus-4-5-\d+$/.test(model)) return "claude-opus-4.5";
8343
+ if (/^claude-opus-4-\d+$/.test(model)) return findLatestModel("claude-opus", "claude-opus-4.5");
8344
+ if (/^claude-haiku-4-5-\d+$/.test(model)) return "claude-haiku-4.5";
8345
+ if (/^claude-haiku-3-5-\d+$/.test(model)) return findLatestModel("claude-haiku", "claude-haiku-4.5");
8346
+ return model;
8937
8347
  }
8938
-
8939
- //#endregion
8940
- //#region src/routes/messages/handler.ts
8941
- function resolveModelFromBetaHeader(model, betaHeader) {
8942
- if (!betaHeader || !/\bcontext-1m\b/.test(betaHeader)) return model;
8943
- if (!model.startsWith("claude-")) return model;
8944
- if (model.endsWith("-1m")) return model;
8945
- const resolved = `${model}-1m`;
8946
- consola.debug(`Detected context-1m in anthropic-beta header, resolving model: ${model} → ${resolved}`);
8947
- return resolved;
8348
+ function translateAnthropicMessagesToOpenAI(anthropicMessages, system, toolNameMapping) {
8349
+ const systemMessages = handleSystemPrompt(system);
8350
+ const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message, toolNameMapping));
8351
+ return [...systemMessages, ...otherMessages];
8948
8352
  }
8949
- async function handleCompletion(c) {
8950
- const rawPayload = await c.req.json();
8951
- consola.debug("Anthropic request payload:", JSON.stringify(rawPayload));
8952
- const { ctx, payload: anthropicPayload } = createEntryContext({
8953
- c,
8954
- rawPayload,
8955
- endpoint: "anthropic",
8956
- normalizePayload: (p) => ({
8957
- ...p,
8958
- model: resolveModelFromBetaHeader(p.model, c.req.header("anthropic-beta"))
8959
- }),
8960
- buildHistoryRequest: (p) => ({
8961
- model: p.model,
8962
- messages: convertAnthropicMessages(p.messages),
8963
- stream: p.stream ?? false,
8964
- tools: p.tools?.map((t) => ({
8965
- name: t.name,
8966
- description: t.description
8967
- })),
8968
- max_tokens: p.max_tokens,
8969
- temperature: p.temperature,
8970
- system: extractSystemPrompt(p.system)
8971
- })
8353
+ const RESERVED_KEYWORDS = ["x-anthropic-billing-header", "x-anthropic-billing"];
8354
+ /**
8355
+ * Filter out reserved keywords from system prompt text.
8356
+ * Copilot API rejects requests containing these keywords.
8357
+ * Removes the entire line containing the keyword to keep the prompt clean.
8358
+ */
8359
+ function filterReservedKeywords(text) {
8360
+ let filtered = text;
8361
+ for (const keyword of RESERVED_KEYWORDS) if (text.includes(keyword)) {
8362
+ consola.debug(`[Reserved Keyword] Removing line containing "${keyword}"`);
8363
+ filtered = filtered.split("\n").filter((line) => !line.includes(keyword)).join("\n");
8364
+ }
8365
+ return filtered;
8366
+ }
8367
+ function handleSystemPrompt(system) {
8368
+ if (!system) return [];
8369
+ if (typeof system === "string") return [{
8370
+ role: "system",
8371
+ content: filterReservedKeywords(system)
8372
+ }];
8373
+ else return [{
8374
+ role: "system",
8375
+ content: filterReservedKeywords(system.map((block) => block.text).join("\n\n"))
8376
+ }];
8377
+ }
8378
+ function handleUserMessage(message) {
8379
+ const newMessages = [];
8380
+ if (Array.isArray(message.content)) {
8381
+ const toolResultBlocks = message.content.filter((block) => block.type === "tool_result");
8382
+ const otherBlocks = message.content.filter((block) => block.type !== "tool_result");
8383
+ for (const block of toolResultBlocks) newMessages.push({
8384
+ role: "tool",
8385
+ tool_call_id: block.tool_use_id,
8386
+ content: mapContent(block.content)
8387
+ });
8388
+ if (otherBlocks.length > 0) newMessages.push({
8389
+ role: "user",
8390
+ content: mapContent(otherBlocks)
8391
+ });
8392
+ } else newMessages.push({
8393
+ role: "user",
8394
+ content: mapContent(message.content)
8972
8395
  });
8973
- const sanitizedPayload = dePoisonAssistantMessages(anthropicPayload);
8974
- logToolInfo(sanitizedPayload);
8975
- const subagentMarker = parseSubagentMarkerFromFirstUser(sanitizedPayload);
8976
- const initiatorOverride = subagentMarker ? "agent" : void 0;
8977
- if (subagentMarker) consola.debug("Detected Subagent marker:", JSON.stringify(subagentMarker));
8978
- if (supportsDirectAnthropicApi(sanitizedPayload.model)) {
8979
- normalizeSystemPromptDate(sanitizedPayload);
8980
- injectSystemCacheControl(sanitizedPayload);
8981
- return handleDirectAnthropicCompletion(c, sanitizedPayload, ctx, initiatorOverride);
8396
+ return newMessages;
8397
+ }
8398
+ function handleAssistantMessage(message, toolNameMapping) {
8399
+ if (!Array.isArray(message.content)) return [{
8400
+ role: "assistant",
8401
+ content: mapContent(message.content)
8402
+ }];
8403
+ const toolUseBlocks = message.content.filter((block) => block.type === "tool_use");
8404
+ const textBlocks = message.content.filter((block) => block.type === "text");
8405
+ const thinkingBlocks = message.content.filter((block) => block.type === "thinking");
8406
+ const allTextContent = [...textBlocks.map((b) => b.text), ...thinkingBlocks.map((b) => b.thinking)].join("\n\n");
8407
+ return toolUseBlocks.length > 0 ? [{
8408
+ role: "assistant",
8409
+ content: allTextContent || null,
8410
+ tool_calls: toolUseBlocks.map((toolUse) => ({
8411
+ id: toolUse.id,
8412
+ type: "function",
8413
+ function: {
8414
+ name: getTruncatedToolName(toolUse.name, toolNameMapping),
8415
+ arguments: JSON.stringify(toolUse.input)
8416
+ }
8417
+ }))
8418
+ }] : [{
8419
+ role: "assistant",
8420
+ content: mapContent(message.content)
8421
+ }];
8422
+ }
8423
+ function mapContent(content) {
8424
+ if (typeof content === "string") return content;
8425
+ if (!Array.isArray(content)) return null;
8426
+ if (!content.some((block) => block.type === "image")) return content.filter((block) => block.type === "text" || block.type === "thinking").map((block) => block.type === "text" ? block.text : block.thinking).join("\n\n");
8427
+ const contentParts = [];
8428
+ for (const block of content) switch (block.type) {
8429
+ case "text":
8430
+ contentParts.push({
8431
+ type: "text",
8432
+ text: block.text
8433
+ });
8434
+ break;
8435
+ case "thinking":
8436
+ contentParts.push({
8437
+ type: "text",
8438
+ text: block.thinking
8439
+ });
8440
+ break;
8441
+ case "image":
8442
+ contentParts.push({
8443
+ type: "image_url",
8444
+ image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
8445
+ });
8446
+ break;
8982
8447
  }
8983
- return handleTranslatedCompletion(c, sanitizedPayload, ctx, initiatorOverride);
8448
+ return contentParts;
8984
8449
  }
8985
- /**
8986
- * Log tool-related information for debugging
8987
- */
8988
- function logToolInfo(anthropicPayload) {
8989
- if (anthropicPayload.tools?.length) {
8990
- const toolInfo = anthropicPayload.tools.map((t) => ({
8991
- name: t.name,
8992
- type: t.type ?? "(custom)"
8993
- }));
8994
- consola.debug(`[Tools] Defined tools:`, JSON.stringify(toolInfo));
8450
+ function getTruncatedToolName(originalName, toolNameMapping) {
8451
+ if (originalName.length <= OPENAI_TOOL_NAME_LIMIT) return originalName;
8452
+ const existingTruncated = toolNameMapping.originalToTruncated.get(originalName);
8453
+ if (existingTruncated) return existingTruncated;
8454
+ let hash = 0;
8455
+ for (let i = 0; i < originalName.length; i++) {
8456
+ const char = originalName.codePointAt(i) ?? 0;
8457
+ hash = (hash << 5) - hash + char;
8458
+ hash = Math.trunc(hash);
8995
8459
  }
8996
- for (const msg of anthropicPayload.messages) if (typeof msg.content !== "string") for (const block of msg.content) {
8997
- if (block.type === "tool_use") consola.debug(`[Tools] tool_use in message: ${block.name} (id: ${block.id})`);
8998
- if (block.type === "tool_result") consola.debug(`[Tools] tool_result in message: id=${block.tool_use_id}, is_error=${block.is_error ?? false}`);
8460
+ const hashSuffix = Math.abs(hash).toString(36).slice(0, 8);
8461
+ const truncatedName = originalName.slice(0, OPENAI_TOOL_NAME_LIMIT - 9) + "_" + hashSuffix;
8462
+ toolNameMapping.originalToTruncated.set(originalName, truncatedName);
8463
+ consola.debug(`Truncated tool name: "${originalName}" -> "${truncatedName}"`);
8464
+ return truncatedName;
8465
+ }
8466
+ function translateAnthropicToolsToOpenAI(anthropicTools, toolNameMapping) {
8467
+ if (!anthropicTools) return;
8468
+ return anthropicTools.map((tool) => ({
8469
+ type: "function",
8470
+ function: {
8471
+ name: getTruncatedToolName(tool.name, toolNameMapping),
8472
+ description: tool.description,
8473
+ parameters: tool.input_schema ?? {}
8474
+ }
8475
+ }));
8476
+ }
8477
+ function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice, toolNameMapping) {
8478
+ if (!anthropicToolChoice) return;
8479
+ switch (anthropicToolChoice.type) {
8480
+ case "auto": return "auto";
8481
+ case "any": return "required";
8482
+ case "tool":
8483
+ if (anthropicToolChoice.name) return {
8484
+ type: "function",
8485
+ function: { name: getTruncatedToolName(anthropicToolChoice.name, toolNameMapping) }
8486
+ };
8487
+ return;
8488
+ case "none": return "none";
8489
+ default: return;
8999
8490
  }
9000
8491
  }
9001
8492
 
@@ -9164,6 +8655,26 @@ const createResponses = async (payload, { vision, initiator, resolvedModel, sign
9164
8655
  return await response.json();
9165
8656
  };
9166
8657
 
8658
+ //#endregion
8659
+ //#region src/routes/responses/model-shortcut.ts
8660
+ /**
8661
+ * Client-facing shortcut aliases for /responses model ids.
8662
+ *
8663
+ * Copilot exposes gpt-5.6 only as three named variants — Luna (lightweight),
8664
+ * Sol (powerful), Terra (versatile). A client sending a bare "gpt-5.6" would
8665
+ * otherwise fail findModelById. Map the shortcut to Sol because upstream tags
8666
+ * it `model_picker_category: "powerful"` — the closest match for an
8667
+ * unqualified "GPT-5.6" ask.
8668
+ *
8669
+ * Applied inside `normalizePayload`, i.e. AFTER `captureRequestedModel`, so
8670
+ * the client-facing echo (ADR-0001) still shows the original "gpt-5.6".
8671
+ */
8672
+ const SHORTCUT_MAP = Object.assign(Object.create(null), { "gpt-5.6": "gpt-5.6-sol" });
8673
+ function resolveResponsesModelShortcut(model) {
8674
+ if (typeof model !== "string") return model;
8675
+ return SHORTCUT_MAP[model.toLowerCase()] ?? model;
8676
+ }
8677
+
9167
8678
  //#endregion
9168
8679
  //#region src/routes/responses/stream-id-sync.ts
9169
8680
  const createStreamIdTracker = () => ({ outputItems: /* @__PURE__ */ new Map() });
@@ -9402,7 +8913,12 @@ const handleResponses = async (c) => {
9402
8913
  rawPayload,
9403
8914
  endpoint: "openai",
9404
8915
  normalizePayload: (p) => {
9405
- const np = state.normalizeResponsesCallIds ? normalizeCallIds(p) : p;
8916
+ const resolvedModel = resolveResponsesModelShortcut(p.model);
8917
+ const withModel = resolvedModel === p.model ? p : {
8918
+ ...p,
8919
+ model: resolvedModel
8920
+ };
8921
+ const np = state.normalizeResponsesCallIds ? normalizeCallIds(withModel) : withModel;
9406
8922
  useFunctionApplyPatch(np);
9407
8923
  filterUnsupportedBuiltins(np);
9408
8924
  injectPromptCacheKey(np, clientName);