@dianshuv/copilot-api 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.mjs +507 -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.0";
1016
1015
 
1017
1016
  //#endregion
1018
1017
  //#region src/lib/event-loop-lag.ts
@@ -3567,9 +3566,8 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
3567
3566
  *
3568
3567
  * Pre-flight steps for any request whose final payload is an OpenAI
3569
3568
  * 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).
3569
+ * and the non-streaming type guard. Used by `routes/chat-completions`
3570
+ * (native OpenAI).
3573
3571
  */
3574
3572
  /** Type guard for non-streaming responses */
3575
3573
  function isNonStreaming(response) {
@@ -3777,89 +3775,6 @@ function createStreamRepetitionChecker(label, config) {
3777
3775
  };
3778
3776
  }
3779
3777
 
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
3778
  //#endregion
3864
3779
  //#region src/services/copilot/create-chat-completions.ts
3865
3780
  const GPT_MODEL_PATTERN = /^gpt-/i;
@@ -3880,21 +3795,15 @@ const createChatCompletions = async (payload, options) => {
3880
3795
  const enableVision = wire.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
3881
3796
  const isAgentCall = wire.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
3882
3797
  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
3798
  const response = await copilotFetch("/chat/completions", {
3896
3799
  method: "POST",
3897
- headers,
3800
+ headers: {
3801
+ ...copilotHeaders(state, {
3802
+ vision: enableVision && modelSupportsVision,
3803
+ modelRequestHeaders: options?.resolvedModel?.request_headers
3804
+ }),
3805
+ "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
3806
+ },
3898
3807
  body: JSON.stringify(wire),
3899
3808
  signal: options?.signal
3900
3809
  });
@@ -4347,12 +4256,12 @@ async function executeRequest(opts) {
4347
4256
  signal: abort.signal
4348
4257
  }));
4349
4258
  ctx.queueWaitMs = queueWaitMs;
4350
- if (isNonStreaming(response)) return handleNonStreamingResponse$1(c, response, ctx, payload);
4259
+ if (isNonStreaming(response)) return handleNonStreamingResponse(c, response, ctx, payload);
4351
4260
  consola.debug("Streaming response");
4352
4261
  updateTrackerStatus(ctx.trackingId, "streaming");
4353
4262
  return streamSSE(c, async (stream) => {
4354
4263
  stream.onAbort(() => abort.abort());
4355
- await handleStreamingResponse$1({
4264
+ await handleStreamingResponse({
4356
4265
  stream,
4357
4266
  response,
4358
4267
  payload,
@@ -4381,7 +4290,7 @@ async function logTokenCount(payload, selectedModel) {
4381
4290
  consola.debug("Failed to calculate token count:", error);
4382
4291
  }
4383
4292
  }
4384
- function handleNonStreamingResponse$1(c, originalResponse, ctx, payload) {
4293
+ function handleNonStreamingResponse(c, originalResponse, ctx, payload) {
4385
4294
  consola.debug("Non-streaming response:", JSON.stringify(originalResponse));
4386
4295
  let response = originalResponse;
4387
4296
  if (state.verbose && ctx.truncateResult?.wasCompacted && response.choices[0]?.message.content) {
@@ -4474,7 +4383,7 @@ function createStreamAccumulator() {
4474
4383
  toolCallMap: /* @__PURE__ */ new Map()
4475
4384
  };
4476
4385
  }
4477
- async function handleStreamingResponse$1(opts) {
4386
+ async function handleStreamingResponse(opts) {
4478
4387
  const { stream, response, payload, ctx } = opts;
4479
4388
  const acc = createStreamAccumulator();
4480
4389
  const checkRepetition = createStreamRepetitionChecker(`openai:${payload.model}`);
@@ -6608,6 +6517,46 @@ async function checkNeedsCompactionAnthropic(payload, model, config = {}) {
6608
6517
  };
6609
6518
  }
6610
6519
 
6520
+ //#endregion
6521
+ //#region src/lib/anthropic/beta.ts
6522
+ /**
6523
+ * Vendor-neutral utilities for manipulating the `anthropic-beta` request header.
6524
+ *
6525
+ * Lives in `lib/anthropic/` (not in either transport module) so both the
6526
+ * Anthropic-native and OpenAI-translated transport layers can share these
6527
+ * helpers without introducing cross-transport imports.
6528
+ */
6529
+ /** Anthropic beta feature that unlocks the 1M context window. */
6530
+ const CONTEXT_1M_BETA_FEATURE = "context-1m-2025-08-07";
6531
+ /**
6532
+ * Merge two comma-separated anthropic-beta header values. Trims whitespace,
6533
+ * drops empty tokens, and dedupes by exact string match. Returns a canonical
6534
+ * comma-joined string with no spaces.
6535
+ *
6536
+ * Either input may be undefined / empty.
6537
+ */
6538
+ function mergeBetaFeatures(existing, incoming) {
6539
+ const seen = /* @__PURE__ */ new Set();
6540
+ const out = [];
6541
+ for (const raw of [existing, incoming]) {
6542
+ if (!raw) continue;
6543
+ for (const part of raw.split(",")) {
6544
+ const f = part.trim();
6545
+ if (f.length === 0 || seen.has(f)) continue;
6546
+ seen.add(f);
6547
+ out.push(f);
6548
+ }
6549
+ }
6550
+ return out.join(",");
6551
+ }
6552
+ /**
6553
+ * Append the context-1m feature to an anthropic-beta header value, deduping
6554
+ * any prior occurrence. Returns the merged comma-separated string.
6555
+ */
6556
+ function appendContext1mBeta(existing) {
6557
+ return mergeBetaFeatures(existing, CONTEXT_1M_BETA_FEATURE);
6558
+ }
6559
+
6611
6560
  //#endregion
6612
6561
  //#region src/lib/anthropic/features.ts
6613
6562
  function normalizeForMatching(modelId) {
@@ -6758,6 +6707,37 @@ function filterServerToolBlocksFromResponse(response) {
6758
6707
  };
6759
6708
  }
6760
6709
 
6710
+ //#endregion
6711
+ //#region src/lib/headers.ts
6712
+ /**
6713
+ * Vendor-neutral header-bag helpers.
6714
+ *
6715
+ * HTTP header names are case-insensitive, but a plain-object header bag is
6716
+ * case-sensitive on its keys. Code that wants to look up "anthropic-beta"
6717
+ * without knowing whether some other producer wrote "Anthropic-Beta" needs
6718
+ * `findHeaderKey`. Code that wants to set a header without creating a
6719
+ * second case variant of the same name needs `setHeader`.
6720
+ */
6721
+ /** Case-insensitive lookup of a header key in a plain-object header bag. */
6722
+ function findHeaderKey(headers, name) {
6723
+ const lower = name.toLowerCase();
6724
+ return Object.keys(headers).find((k) => k.toLowerCase() === lower);
6725
+ }
6726
+ /** Case-insensitive read of a header value. */
6727
+ function getHeader(headers, name) {
6728
+ const key = findHeaderKey(headers, name);
6729
+ return key === void 0 ? void 0 : headers[key];
6730
+ }
6731
+ /**
6732
+ * Set a header value at the existing case variant if one is present, else at
6733
+ * the supplied canonical name. Prevents a second key (different case) from
6734
+ * being added for the same logical header.
6735
+ */
6736
+ function setHeader(headers, name, value) {
6737
+ const key = findHeaderKey(headers, name) ?? name;
6738
+ headers[key] = value;
6739
+ }
6740
+
6761
6741
  //#endregion
6762
6742
  //#region src/services/copilot/create-anthropic-messages.ts
6763
6743
  /**
@@ -6960,11 +6940,12 @@ function resolveAnthropicModelForDirectPath(modelId) {
6960
6940
  }
6961
6941
  }
6962
6942
  /**
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.
6943
+ * Check if a model supports the native direct Anthropic API on Copilot.
6944
+ * True iff the model resolves to an Anthropic-vendor model (see
6945
+ * resolveAnthropicModelForDirectPath). `/v1/messages` serves these only;
6946
+ * the OpenAI-translation fallback was removed (docs/adr/0004-...).
6965
6947
  */
6966
6948
  function supportsDirectAnthropicApi(modelId) {
6967
- if (state.redirectAnthropic) return false;
6968
6949
  return resolveAnthropicModelForDirectPath(modelId) !== void 0;
6969
6950
  }
6970
6951
 
@@ -7161,15 +7142,6 @@ function extractToolCallsFromContent(content) {
7161
7142
  });
7162
7143
  return tools.length > 0 ? tools : void 0;
7163
7144
  }
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
7145
  function prependMarkerToResponse(response, marker) {
7174
7146
  if (!marker) return response;
7175
7147
  const content = [...response.content];
@@ -7290,555 +7262,79 @@ function recordAnthropicStreamingResponse(acc, fallbackModel, ctx) {
7290
7262
  }
7291
7263
 
7292
7264
  //#endregion
7293
- //#region src/routes/messages/non-stream-translation.ts
7294
- const OPENAI_TOOL_NAME_LIMIT = 64;
7265
+ //#region src/routes/messages/stream-translation.ts
7295
7266
  /**
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
7267
+ * Wrap an arbitrary error into an Anthropic-native `error` stream event.
7301
7268
  *
7302
- * Adding placeholder responses prevents API errors and maintains protocol compliance.
7269
+ * Shared by the direct Anthropic path (`direct-anthropic-handler.ts`) to emit a
7270
+ * client-facing error frame mid-stream. The OpenAI→Anthropic response
7271
+ * translation that once also lived here was removed with the translation
7272
+ * fallback (see docs/adr/0004-drop-openai-translation-fallback-for-messages.md).
7303
7273
  */
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
- }
7274
+ function translateErrorToAnthropicErrorEvent(error) {
7275
+ return {
7276
+ type: "error",
7277
+ error: {
7278
+ type: "api_error",
7279
+ message: error ? formatError(error) : "An unexpected error occurred during streaming."
7325
7280
  }
7281
+ };
7282
+ }
7283
+
7284
+ //#endregion
7285
+ //#region src/routes/messages/tool-call-recovery.ts
7286
+ const ENVELOPE = String.raw`(?:<(?:antml:)?function_calls>|call|count|court)`;
7287
+ const INVOKE_BODY = String.raw`<(?:antml:)?invoke\s+name="[^"]+">(?:(?!<(?:antml:)?invoke\b)[\s\S])*?</(?:antml:)?invoke>`;
7288
+ 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");
7289
+ const STARTS_WITH_ENVELOPE_RE = new RegExp(String.raw`^[ \t\n]*` + ENVELOPE);
7290
+ const INVOKE_OPENER_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*)?<(?:antml:)?invoke\b[^\n>]*>?`, "g");
7291
+ const LEAK_OPEN_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*)?<(?:antml:)?invoke\s+name="`, "g");
7292
+ const INVOKE_RE = /<(?:antml:)?invoke\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?invoke>/g;
7293
+ const PARAMETER_RE = /<(?:antml:)?parameter\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?parameter>/g;
7294
+ function coerceParamValue(raw) {
7295
+ const trimmed = raw.trim();
7296
+ if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) try {
7297
+ return JSON.parse(trimmed);
7298
+ } catch {
7299
+ return raw;
7326
7300
  }
7327
- return fixedMessages;
7301
+ return raw;
7328
7302
  }
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
- };
7303
+ function parseRegionInvokes(region, knownTools) {
7304
+ const calls = [];
7305
+ for (const invokeMatch of region.matchAll(INVOKE_RE)) {
7306
+ const name = invokeMatch[1];
7307
+ if (knownTools && !knownTools.has(name)) continue;
7308
+ const input = {};
7309
+ for (const paramMatch of invokeMatch[2].matchAll(PARAMETER_RE)) input[paramMatch[1]] = coerceParamValue(paramMatch[2]);
7310
+ calls.push({
7311
+ name,
7312
+ input
7313
+ });
7314
+ }
7315
+ return calls;
7316
+ }
7317
+ const FENCE_DELIM_RE = /(?:^|\n)[ \t]*```/g;
7318
+ function insideFence(text, pos) {
7319
+ let openAt = -1;
7320
+ for (const m of text.matchAll(FENCE_DELIM_RE)) if (openAt === -1) {
7321
+ if (m.index >= pos) break;
7322
+ openAt = m.index;
7323
+ } else if (m.index > pos) return true;
7324
+ else openAt = -1;
7325
+ return false;
7326
+ }
7327
+ function regionIsLeak(region, offset, fullText, knownTools) {
7328
+ if (insideFence(fullText, offset)) return false;
7329
+ if (STARTS_WITH_ENVELOPE_RE.test(region)) return true;
7330
+ if (!knownTools) return false;
7331
+ return parseRegionInvokes(region, knownTools).length > 0;
7350
7332
  }
7351
7333
  /**
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.
7334
+ * Split assistant text into ordered segments dropping leaked envelope markup
7335
+ * (and undeclared-tool invokes) while preserving the natural-language on either
7336
+ * side and the pre/tool/post ordering. Returns a single text segment when there
7337
+ * is no leak. Shared by both response recovery paths so they cannot diverge.
7842
7338
  *
7843
7339
  * `from` restricts the EMITTED window to `text.slice(from)` while still
7844
7340
  * classifying against the FULL `text` — so a streaming capture that started just
@@ -8643,359 +8139,350 @@ const parseSubagentMarkerFromSystemReminder = (text) => {
8643
8139
  };
8644
8140
 
8645
8141
  //#endregion
8646
- //#region src/routes/messages/translated-handler.ts
8142
+ //#region src/routes/messages/handler.ts
8143
+ function resolveModelFromBetaHeader(model, betaHeader) {
8144
+ if (!betaHeader || !/\bcontext-1m\b/.test(betaHeader)) return model;
8145
+ if (!model.startsWith("claude-")) return model;
8146
+ if (model.endsWith("-1m")) return model;
8147
+ const resolved = `${model}-1m`;
8148
+ consola.debug(`Detected context-1m in anthropic-beta header, resolving model: ${model} → ${resolved}`);
8149
+ return resolved;
8150
+ }
8151
+ async function handleCompletion(c) {
8152
+ const rawPayload = await c.req.json();
8153
+ consola.debug("Anthropic request payload:", JSON.stringify(rawPayload));
8154
+ if (rawPayload === null || typeof rawPayload.model !== "string" || rawPayload.model.length === 0) return c.json({
8155
+ type: "error",
8156
+ error: {
8157
+ type: "invalid_request_error",
8158
+ message: "model is required and must be a non-empty string"
8159
+ }
8160
+ }, 400);
8161
+ const normalizedModel = resolveModelFromBetaHeader(rawPayload.model, c.req.header("anthropic-beta"));
8162
+ if (!supportsDirectAnthropicApi(normalizedModel)) return c.json({
8163
+ type: "error",
8164
+ error: {
8165
+ type: "invalid_request_error",
8166
+ message: `model \`${normalizedModel}\` is not an Anthropic model available on /v1/messages`
8167
+ }
8168
+ }, 400);
8169
+ const { ctx, payload: anthropicPayload } = createEntryContext({
8170
+ c,
8171
+ rawPayload,
8172
+ endpoint: "anthropic",
8173
+ normalizePayload: (p) => ({
8174
+ ...p,
8175
+ model: resolveModelFromBetaHeader(p.model, c.req.header("anthropic-beta"))
8176
+ }),
8177
+ buildHistoryRequest: (p) => ({
8178
+ model: p.model,
8179
+ messages: convertAnthropicMessages(p.messages),
8180
+ stream: p.stream ?? false,
8181
+ tools: p.tools?.map((t) => ({
8182
+ name: t.name,
8183
+ description: t.description
8184
+ })),
8185
+ max_tokens: p.max_tokens,
8186
+ temperature: p.temperature,
8187
+ system: extractSystemPrompt(p.system)
8188
+ })
8189
+ });
8190
+ const sanitizedPayload = dePoisonAssistantMessages(anthropicPayload);
8191
+ logToolInfo(sanitizedPayload);
8192
+ const subagentMarker = parseSubagentMarkerFromFirstUser(sanitizedPayload);
8193
+ const initiatorOverride = subagentMarker ? "agent" : void 0;
8194
+ if (subagentMarker) consola.debug("Detected Subagent marker:", JSON.stringify(subagentMarker));
8195
+ normalizeSystemPromptDate(sanitizedPayload);
8196
+ injectSystemCacheControl(sanitizedPayload);
8197
+ return handleDirectAnthropicCompletion(c, sanitizedPayload, ctx, initiatorOverride);
8198
+ }
8647
8199
  /**
8648
- * Handle completion using OpenAI translation path (legacy)
8200
+ * Log tool-related information for debugging
8649
8201
  */
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
8202
+ function logToolInfo(anthropicPayload) {
8203
+ if (anthropicPayload.tools?.length) {
8204
+ const toolInfo = anthropicPayload.tools.map((t) => ({
8205
+ name: t.name,
8206
+ type: t.type ?? "(custom)"
8207
+ }));
8208
+ consola.debug(`[Tools] Defined tools:`, JSON.stringify(toolInfo));
8209
+ }
8210
+ for (const msg of anthropicPayload.messages) if (typeof msg.content !== "string") for (const block of msg.content) {
8211
+ if (block.type === "tool_use") consola.debug(`[Tools] tool_use in message: ${block.name} (id: ${block.id})`);
8212
+ if (block.type === "tool_result") consola.debug(`[Tools] tool_result in message: id=${block.tool_use_id}, is_error=${block.is_error ?? false}`);
8213
+ }
8214
+ }
8215
+
8216
+ //#endregion
8217
+ //#region src/routes/messages/non-stream-translation.ts
8218
+ const OPENAI_TOOL_NAME_LIMIT = 64;
8219
+ /**
8220
+ * Ensure all tool_use blocks have corresponding tool_result responses.
8221
+ * This handles edge cases where conversation history may be incomplete:
8222
+ * - Session interruptions where tool execution was cut off
8223
+ * - Previous request failures
8224
+ * - Client sending truncated history
8225
+ *
8226
+ * Adding placeholder responses prevents API errors and maintains protocol compliance.
8227
+ */
8228
+ function fixMessageSequence(messages) {
8229
+ const fixedMessages = [];
8230
+ for (let i = 0; i < messages.length; i++) {
8231
+ const message = messages[i];
8232
+ fixedMessages.push(message);
8233
+ if (message.role === "assistant" && message.tool_calls && message.tool_calls.length > 0) {
8234
+ const foundToolResponses = /* @__PURE__ */ new Set();
8235
+ let j = i + 1;
8236
+ while (j < messages.length && messages[j].role === "tool") {
8237
+ const toolMessage = messages[j];
8238
+ if (toolMessage.tool_call_id) foundToolResponses.add(toolMessage.tool_call_id);
8239
+ j++;
8240
+ }
8241
+ for (const toolCall of message.tool_calls) if (!foundToolResponses.has(toolCall.id)) {
8242
+ consola.debug(`Adding placeholder tool_result for ${toolCall.id}`);
8243
+ fixedMessages.push({
8244
+ role: "tool",
8245
+ tool_call_id: toolCall.id,
8246
+ content: "Tool execution was interrupted or failed."
8704
8247
  });
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 });
8248
+ }
8753
8249
  }
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
8250
  }
8251
+ return fixedMessages;
8758
8252
  }
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
8253
+ function translateToOpenAI(payload) {
8254
+ const toolNameMapping = { originalToTruncated: /* @__PURE__ */ new Map() };
8255
+ const messages = translateAnthropicMessagesToOpenAI(payload.messages, payload.system, toolNameMapping);
8256
+ return { payload: {
8257
+ model: translateModelName(payload.model),
8258
+ messages: fixMessageSequence(messages),
8259
+ max_tokens: payload.max_tokens,
8260
+ stop: payload.stop_sequences,
8261
+ stream: payload.stream,
8262
+ temperature: payload.temperature,
8263
+ top_p: payload.top_p,
8264
+ user: payload.metadata?.user_id,
8265
+ tools: translateAnthropicToolsToOpenAI(payload.tools, toolNameMapping),
8266
+ tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice, toolNameMapping)
8267
+ } };
8268
+ }
8269
+ /**
8270
+ * Find the latest available model matching a family prefix.
8271
+ * Searches state.models for models starting with the given prefix
8272
+ * and returns the one with the highest version number.
8273
+ *
8274
+ * @param familyPrefix - e.g., "claude-opus", "claude-sonnet", "claude-haiku"
8275
+ * @param fallback - fallback model ID if no match found
8276
+ */
8277
+ function findLatestModel(familyPrefix, fallback) {
8278
+ const models = state.models?.data;
8279
+ if (!models || models.length === 0) return fallback;
8280
+ const candidates = models.filter((m) => m.id.startsWith(familyPrefix));
8281
+ if (candidates.length === 0) return fallback;
8282
+ candidates.sort((a, b) => {
8283
+ const [aMajor, aMinor] = extractVersion(a.id, familyPrefix);
8284
+ const [bMajor, bMinor] = extractVersion(b.id, familyPrefix);
8285
+ if (aMajor !== bMajor) return bMajor - aMajor;
8286
+ return bMinor - aMinor;
8814
8287
  });
8815
- return c.json(echoResponseBody(anthropicResponse, ctx));
8288
+ return candidates[0].id;
8816
8289
  }
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
- }
8290
+ /**
8291
+ * Extract numeric [major, minor] version from a model id.
8292
+ *
8293
+ * Supports both naming conventions Anthropic/Copilot have used:
8294
+ * - dot: "claude-opus-4.5" → [4, 5]
8295
+ * - dash: "claude-opus-4-8" → [4, 8]
8296
+ * - dash double-digit: "claude-opus-4-10" → [4, 10]
8297
+ *
8298
+ * The dash form previously parsed as just the major via the regex
8299
+ * /^(\d+(?:\.\d+)?)/ (the dash stopped the match), which silently
8300
+ * downgraded dash-named candidates against any dot-named candidate in
8301
+ * findLatestModel. Parsing into a tuple also avoids the parseFloat
8302
+ * lossiness on double-digit minors ("4.10" → 4.1).
8303
+ *
8304
+ * Anything after the major/minor segment (date stamps, "-1m") is ignored.
8305
+ * The minor capture is bounded to 1-3 digits so that an 8-digit date suffix
8306
+ * directly after the major (e.g. "claude-opus-4-20250514") is NOT mistaken
8307
+ * for a minor version of 20_250_514 — without that bound, dated ids would
8308
+ * outrank legitimate dotted candidates like "claude-opus-4.8" in
8309
+ * findLatestModel's sort.
8310
+ *
8311
+ * Returns [0, 0] when no version can be extracted.
8312
+ */
8313
+ function extractVersion(modelId, prefix) {
8314
+ const match = modelId.slice(prefix.length + 1).match(/^(\d+)(?:[.-](\d{1,3}))?/);
8315
+ if (!match) return [0, 0];
8316
+ const major = Number.parseInt(match[1], 10);
8317
+ const rawMinor = match[2];
8318
+ return [major, rawMinor === void 0 ? 0 : Number.parseInt(rawMinor, 10) || 0];
8875
8319
  }
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
8320
+ function translateModelName(model) {
8321
+ const aliasMap = {
8322
+ opus: "claude-opus",
8323
+ sonnet: "claude-sonnet",
8324
+ haiku: "claude-haiku"
8904
8325
  };
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
- }
8326
+ if (aliasMap[model]) {
8327
+ const familyPrefix = aliasMap[model];
8328
+ return findLatestModel(familyPrefix, `${familyPrefix}-4.5`);
8936
8329
  }
8330
+ if (/^claude-sonnet-4-5-\d+$/.test(model)) return "claude-sonnet-4.5";
8331
+ if (/^claude-sonnet-4-\d+$/.test(model)) return "claude-sonnet-4";
8332
+ if (model === "claude-opus-4-8-1m") return "claude-opus-4.8";
8333
+ if (model === "claude-opus-4-8") return "claude-opus-4.8";
8334
+ if (model === "claude-opus-4-7-1m") return "claude-opus-4.7";
8335
+ if (/^claude-opus-4-7$/.test(model)) return "claude-opus-4.7";
8336
+ if (model === "claude-opus-4-6-1m") return "claude-opus-4.6-1m";
8337
+ if (/^claude-opus-4-6$/.test(model)) return "claude-opus-4.6";
8338
+ if (/^claude-opus-4-5-\d+$/.test(model)) return "claude-opus-4.5";
8339
+ if (/^claude-opus-4-\d+$/.test(model)) return findLatestModel("claude-opus", "claude-opus-4.5");
8340
+ if (/^claude-haiku-4-5-\d+$/.test(model)) return "claude-haiku-4.5";
8341
+ if (/^claude-haiku-3-5-\d+$/.test(model)) return findLatestModel("claude-haiku", "claude-haiku-4.5");
8342
+ return model;
8937
8343
  }
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;
8344
+ function translateAnthropicMessagesToOpenAI(anthropicMessages, system, toolNameMapping) {
8345
+ const systemMessages = handleSystemPrompt(system);
8346
+ const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message, toolNameMapping));
8347
+ return [...systemMessages, ...otherMessages];
8948
8348
  }
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
- })
8349
+ const RESERVED_KEYWORDS = ["x-anthropic-billing-header", "x-anthropic-billing"];
8350
+ /**
8351
+ * Filter out reserved keywords from system prompt text.
8352
+ * Copilot API rejects requests containing these keywords.
8353
+ * Removes the entire line containing the keyword to keep the prompt clean.
8354
+ */
8355
+ function filterReservedKeywords(text) {
8356
+ let filtered = text;
8357
+ for (const keyword of RESERVED_KEYWORDS) if (text.includes(keyword)) {
8358
+ consola.debug(`[Reserved Keyword] Removing line containing "${keyword}"`);
8359
+ filtered = filtered.split("\n").filter((line) => !line.includes(keyword)).join("\n");
8360
+ }
8361
+ return filtered;
8362
+ }
8363
+ function handleSystemPrompt(system) {
8364
+ if (!system) return [];
8365
+ if (typeof system === "string") return [{
8366
+ role: "system",
8367
+ content: filterReservedKeywords(system)
8368
+ }];
8369
+ else return [{
8370
+ role: "system",
8371
+ content: filterReservedKeywords(system.map((block) => block.text).join("\n\n"))
8372
+ }];
8373
+ }
8374
+ function handleUserMessage(message) {
8375
+ const newMessages = [];
8376
+ if (Array.isArray(message.content)) {
8377
+ const toolResultBlocks = message.content.filter((block) => block.type === "tool_result");
8378
+ const otherBlocks = message.content.filter((block) => block.type !== "tool_result");
8379
+ for (const block of toolResultBlocks) newMessages.push({
8380
+ role: "tool",
8381
+ tool_call_id: block.tool_use_id,
8382
+ content: mapContent(block.content)
8383
+ });
8384
+ if (otherBlocks.length > 0) newMessages.push({
8385
+ role: "user",
8386
+ content: mapContent(otherBlocks)
8387
+ });
8388
+ } else newMessages.push({
8389
+ role: "user",
8390
+ content: mapContent(message.content)
8972
8391
  });
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);
8392
+ return newMessages;
8393
+ }
8394
+ function handleAssistantMessage(message, toolNameMapping) {
8395
+ if (!Array.isArray(message.content)) return [{
8396
+ role: "assistant",
8397
+ content: mapContent(message.content)
8398
+ }];
8399
+ const toolUseBlocks = message.content.filter((block) => block.type === "tool_use");
8400
+ const textBlocks = message.content.filter((block) => block.type === "text");
8401
+ const thinkingBlocks = message.content.filter((block) => block.type === "thinking");
8402
+ const allTextContent = [...textBlocks.map((b) => b.text), ...thinkingBlocks.map((b) => b.thinking)].join("\n\n");
8403
+ return toolUseBlocks.length > 0 ? [{
8404
+ role: "assistant",
8405
+ content: allTextContent || null,
8406
+ tool_calls: toolUseBlocks.map((toolUse) => ({
8407
+ id: toolUse.id,
8408
+ type: "function",
8409
+ function: {
8410
+ name: getTruncatedToolName(toolUse.name, toolNameMapping),
8411
+ arguments: JSON.stringify(toolUse.input)
8412
+ }
8413
+ }))
8414
+ }] : [{
8415
+ role: "assistant",
8416
+ content: mapContent(message.content)
8417
+ }];
8418
+ }
8419
+ function mapContent(content) {
8420
+ if (typeof content === "string") return content;
8421
+ if (!Array.isArray(content)) return null;
8422
+ 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");
8423
+ const contentParts = [];
8424
+ for (const block of content) switch (block.type) {
8425
+ case "text":
8426
+ contentParts.push({
8427
+ type: "text",
8428
+ text: block.text
8429
+ });
8430
+ break;
8431
+ case "thinking":
8432
+ contentParts.push({
8433
+ type: "text",
8434
+ text: block.thinking
8435
+ });
8436
+ break;
8437
+ case "image":
8438
+ contentParts.push({
8439
+ type: "image_url",
8440
+ image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
8441
+ });
8442
+ break;
8982
8443
  }
8983
- return handleTranslatedCompletion(c, sanitizedPayload, ctx, initiatorOverride);
8444
+ return contentParts;
8984
8445
  }
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));
8446
+ function getTruncatedToolName(originalName, toolNameMapping) {
8447
+ if (originalName.length <= OPENAI_TOOL_NAME_LIMIT) return originalName;
8448
+ const existingTruncated = toolNameMapping.originalToTruncated.get(originalName);
8449
+ if (existingTruncated) return existingTruncated;
8450
+ let hash = 0;
8451
+ for (let i = 0; i < originalName.length; i++) {
8452
+ const char = originalName.codePointAt(i) ?? 0;
8453
+ hash = (hash << 5) - hash + char;
8454
+ hash = Math.trunc(hash);
8995
8455
  }
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}`);
8456
+ const hashSuffix = Math.abs(hash).toString(36).slice(0, 8);
8457
+ const truncatedName = originalName.slice(0, OPENAI_TOOL_NAME_LIMIT - 9) + "_" + hashSuffix;
8458
+ toolNameMapping.originalToTruncated.set(originalName, truncatedName);
8459
+ consola.debug(`Truncated tool name: "${originalName}" -> "${truncatedName}"`);
8460
+ return truncatedName;
8461
+ }
8462
+ function translateAnthropicToolsToOpenAI(anthropicTools, toolNameMapping) {
8463
+ if (!anthropicTools) return;
8464
+ return anthropicTools.map((tool) => ({
8465
+ type: "function",
8466
+ function: {
8467
+ name: getTruncatedToolName(tool.name, toolNameMapping),
8468
+ description: tool.description,
8469
+ parameters: tool.input_schema ?? {}
8470
+ }
8471
+ }));
8472
+ }
8473
+ function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice, toolNameMapping) {
8474
+ if (!anthropicToolChoice) return;
8475
+ switch (anthropicToolChoice.type) {
8476
+ case "auto": return "auto";
8477
+ case "any": return "required";
8478
+ case "tool":
8479
+ if (anthropicToolChoice.name) return {
8480
+ type: "function",
8481
+ function: { name: getTruncatedToolName(anthropicToolChoice.name, toolNameMapping) }
8482
+ };
8483
+ return;
8484
+ case "none": return "none";
8485
+ default: return;
8999
8486
  }
9000
8487
  }
9001
8488
 
@@ -9164,6 +8651,26 @@ const createResponses = async (payload, { vision, initiator, resolvedModel, sign
9164
8651
  return await response.json();
9165
8652
  };
9166
8653
 
8654
+ //#endregion
8655
+ //#region src/routes/responses/model-shortcut.ts
8656
+ /**
8657
+ * Client-facing shortcut aliases for /responses model ids.
8658
+ *
8659
+ * Copilot exposes gpt-5.6 only as three named variants — Luna (lightweight),
8660
+ * Sol (powerful), Terra (versatile). A client sending a bare "gpt-5.6" would
8661
+ * otherwise fail findModelById. Map the shortcut to Sol because upstream tags
8662
+ * it `model_picker_category: "powerful"` — the closest match for an
8663
+ * unqualified "GPT-5.6" ask.
8664
+ *
8665
+ * Applied inside `normalizePayload`, i.e. AFTER `captureRequestedModel`, so
8666
+ * the client-facing echo (ADR-0001) still shows the original "gpt-5.6".
8667
+ */
8668
+ const SHORTCUT_MAP = Object.assign(Object.create(null), { "gpt-5.6": "gpt-5.6-sol" });
8669
+ function resolveResponsesModelShortcut(model) {
8670
+ if (typeof model !== "string") return model;
8671
+ return SHORTCUT_MAP[model.toLowerCase()] ?? model;
8672
+ }
8673
+
9167
8674
  //#endregion
9168
8675
  //#region src/routes/responses/stream-id-sync.ts
9169
8676
  const createStreamIdTracker = () => ({ outputItems: /* @__PURE__ */ new Map() });
@@ -9402,7 +8909,12 @@ const handleResponses = async (c) => {
9402
8909
  rawPayload,
9403
8910
  endpoint: "openai",
9404
8911
  normalizePayload: (p) => {
9405
- const np = state.normalizeResponsesCallIds ? normalizeCallIds(p) : p;
8912
+ const resolvedModel = resolveResponsesModelShortcut(p.model);
8913
+ const withModel = resolvedModel === p.model ? p : {
8914
+ ...p,
8915
+ model: resolvedModel
8916
+ };
8917
+ const np = state.normalizeResponsesCallIds ? normalizeCallIds(withModel) : withModel;
9406
8918
  useFunctionApplyPatch(np);
9407
8919
  filterUnsupportedBuiltins(np);
9408
8920
  injectPromptCacheKey(np, clientName);