@dianshuv/copilot-api 0.18.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 +527 -996
  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.18.0";
1014
+ var version = "0.20.0";
1016
1015
 
1017
1016
  //#endregion
1018
1017
  //#region src/lib/event-loop-lag.ts
@@ -1595,6 +1594,7 @@ function captureRequest(params) {
1595
1594
  };
1596
1595
  if (params.reasoningTokens !== void 0) properties.reasoning_tokens = params.reasoningTokens;
1597
1596
  if (params.cachedInputTokens !== void 0) properties.cached_input_tokens = params.cachedInputTokens;
1597
+ if (params.cacheCreationInputTokens !== void 0) properties.cache_creation_input_tokens = params.cacheCreationInputTokens;
1598
1598
  if (params.totalInputTokens !== void 0) properties.total_input_tokens = params.totalInputTokens;
1599
1599
  if (params.stopReason !== void 0) properties.stop_reason = params.stopReason;
1600
1600
  if (params.status !== void 0) properties.status = params.status;
@@ -3002,6 +3002,7 @@ var RequestTracker = class {
3002
3002
  if (update.outputTokens !== void 0) request.outputTokens = update.outputTokens;
3003
3003
  if (update.reasoningTokens !== void 0) request.reasoningTokens = update.reasoningTokens;
3004
3004
  if (update.cachedInputTokens !== void 0) request.cachedInputTokens = update.cachedInputTokens;
3005
+ if (update.cacheCreationInputTokens !== void 0) request.cacheCreationInputTokens = update.cacheCreationInputTokens;
3005
3006
  if (update.totalInputTokens !== void 0) request.totalInputTokens = update.totalInputTokens;
3006
3007
  if (update.error !== void 0) request.error = update.error;
3007
3008
  if (update.queuePosition !== void 0) request.queuePosition = update.queuePosition;
@@ -3565,9 +3566,8 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
3565
3566
  *
3566
3567
  * Pre-flight steps for any request whose final payload is an OpenAI
3567
3568
  * ChatCompletionsPayload — auto-truncate decisions, 413 diagnostic logging,
3568
- * and the non-streaming type guard. Used by both `routes/chat-completions`
3569
- * (native OpenAI) and `routes/messages/translated-handler` (Anthropic that
3570
- * gets translated into OpenAI shape before hitting upstream).
3569
+ * and the non-streaming type guard. Used by `routes/chat-completions`
3570
+ * (native OpenAI).
3571
3571
  */
3572
3572
  /** Type guard for non-streaming responses */
3573
3573
  function isNonStreaming(response) {
@@ -3775,89 +3775,6 @@ function createStreamRepetitionChecker(label, config) {
3775
3775
  };
3776
3776
  }
3777
3777
 
3778
- //#endregion
3779
- //#region src/lib/anthropic/beta.ts
3780
- /**
3781
- * Vendor-neutral utilities for manipulating the `anthropic-beta` request header.
3782
- *
3783
- * Lives in `lib/anthropic/` (not in either transport module) so both the
3784
- * Anthropic-native and OpenAI-translated transport layers can share these
3785
- * helpers without introducing cross-transport imports.
3786
- */
3787
- /** Anthropic beta feature that unlocks the 1M context window. */
3788
- const CONTEXT_1M_BETA_FEATURE = "context-1m-2025-08-07";
3789
- /**
3790
- * Merge two comma-separated anthropic-beta header values. Trims whitespace,
3791
- * drops empty tokens, and dedupes by exact string match. Returns a canonical
3792
- * comma-joined string with no spaces.
3793
- *
3794
- * Either input may be undefined / empty.
3795
- */
3796
- function mergeBetaFeatures(existing, incoming) {
3797
- const seen = /* @__PURE__ */ new Set();
3798
- const out = [];
3799
- for (const raw of [existing, incoming]) {
3800
- if (!raw) continue;
3801
- for (const part of raw.split(",")) {
3802
- const f = part.trim();
3803
- if (f.length === 0 || seen.has(f)) continue;
3804
- seen.add(f);
3805
- out.push(f);
3806
- }
3807
- }
3808
- return out.join(",");
3809
- }
3810
- /**
3811
- * Append the context-1m feature to an anthropic-beta header value, deduping
3812
- * any prior occurrence. Returns the merged comma-separated string.
3813
- */
3814
- function appendContext1mBeta(existing) {
3815
- return mergeBetaFeatures(existing, CONTEXT_1M_BETA_FEATURE);
3816
- }
3817
- /**
3818
- * True iff a model id appears to be the suffixed 1M-context variant of an
3819
- * Anthropic Claude model (e.g. claude-opus-4-8-1m, claude-opus-4.6-1m).
3820
- *
3821
- * Used as a state.models-independent signal for whether to inject the
3822
- * context-1m-2025-08-07 beta header, so the 1M intent survives a stale or
3823
- * empty model cache (where `resolveAnthropicModelForDirectPath` would return
3824
- * undefined). Forwarding the beta is harmless to upstreams that ignore it.
3825
- */
3826
- function isOneMillionSuffixedClaudeId(modelId) {
3827
- return modelId.startsWith("claude-") && modelId.endsWith("-1m");
3828
- }
3829
-
3830
- //#endregion
3831
- //#region src/lib/headers.ts
3832
- /**
3833
- * Vendor-neutral header-bag helpers.
3834
- *
3835
- * HTTP header names are case-insensitive, but a plain-object header bag is
3836
- * case-sensitive on its keys. Code that wants to look up "anthropic-beta"
3837
- * without knowing whether some other producer wrote "Anthropic-Beta" needs
3838
- * `findHeaderKey`. Code that wants to set a header without creating a
3839
- * second case variant of the same name needs `setHeader`.
3840
- */
3841
- /** Case-insensitive lookup of a header key in a plain-object header bag. */
3842
- function findHeaderKey(headers, name) {
3843
- const lower = name.toLowerCase();
3844
- return Object.keys(headers).find((k) => k.toLowerCase() === lower);
3845
- }
3846
- /** Case-insensitive read of a header value. */
3847
- function getHeader(headers, name) {
3848
- const key = findHeaderKey(headers, name);
3849
- return key === void 0 ? void 0 : headers[key];
3850
- }
3851
- /**
3852
- * Set a header value at the existing case variant if one is present, else at
3853
- * the supplied canonical name. Prevents a second key (different case) from
3854
- * being added for the same logical header.
3855
- */
3856
- function setHeader(headers, name, value) {
3857
- const key = findHeaderKey(headers, name) ?? name;
3858
- headers[key] = value;
3859
- }
3860
-
3861
3778
  //#endregion
3862
3779
  //#region src/services/copilot/create-chat-completions.ts
3863
3780
  const GPT_MODEL_PATTERN = /^gpt-/i;
@@ -3878,21 +3795,15 @@ const createChatCompletions = async (payload, options) => {
3878
3795
  const enableVision = wire.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
3879
3796
  const isAgentCall = wire.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
3880
3797
  const modelSupportsVision = options?.resolvedModel?.capabilities?.supports?.vision !== false;
3881
- const headers = {
3882
- ...copilotHeaders(state, {
3883
- vision: enableVision && modelSupportsVision,
3884
- modelRequestHeaders: options?.resolvedModel?.request_headers
3885
- }),
3886
- "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
3887
- };
3888
- if (options?.anthropicBeta) {
3889
- const existingKey = findHeaderKey(headers, "anthropic-beta") ?? "anthropic-beta";
3890
- headers[existingKey] = mergeBetaFeatures(headers[existingKey], options.anthropicBeta);
3891
- consola.debug(`[ChatCompletions] anthropic-beta after merge: ${headers[existingKey]}`);
3892
- }
3893
3798
  const response = await copilotFetch("/chat/completions", {
3894
3799
  method: "POST",
3895
- 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
+ },
3896
3807
  body: JSON.stringify(wire),
3897
3808
  signal: options?.signal
3898
3809
  });
@@ -4178,6 +4089,7 @@ function completeTracking(trackingId, inputTokens, outputTokens, queueWaitMs, re
4178
4089
  queueWaitMs,
4179
4090
  reasoningTokens,
4180
4091
  cachedInputTokens: cache?.cachedInputTokens,
4092
+ cacheCreationInputTokens: cache?.cacheCreationInputTokens,
4181
4093
  totalInputTokens: cache?.totalInputTokens,
4182
4094
  ...timingsToUpdate(timings)
4183
4095
  });
@@ -4196,6 +4108,7 @@ function completeTracking(trackingId, inputTokens, outputTokens, queueWaitMs, re
4196
4108
  toolCount: analytics.toolCount ?? 0,
4197
4109
  reasoningTokens,
4198
4110
  cachedInputTokens: cache?.cachedInputTokens,
4111
+ cacheCreationInputTokens: cache?.cacheCreationInputTokens,
4199
4112
  totalInputTokens: cache?.totalInputTokens,
4200
4113
  stopReason: analytics.stopReason
4201
4114
  });
@@ -4343,12 +4256,12 @@ async function executeRequest(opts) {
4343
4256
  signal: abort.signal
4344
4257
  }));
4345
4258
  ctx.queueWaitMs = queueWaitMs;
4346
- if (isNonStreaming(response)) return handleNonStreamingResponse$1(c, response, ctx, payload);
4259
+ if (isNonStreaming(response)) return handleNonStreamingResponse(c, response, ctx, payload);
4347
4260
  consola.debug("Streaming response");
4348
4261
  updateTrackerStatus(ctx.trackingId, "streaming");
4349
4262
  return streamSSE(c, async (stream) => {
4350
4263
  stream.onAbort(() => abort.abort());
4351
- await handleStreamingResponse$1({
4264
+ await handleStreamingResponse({
4352
4265
  stream,
4353
4266
  response,
4354
4267
  payload,
@@ -4377,7 +4290,7 @@ async function logTokenCount(payload, selectedModel) {
4377
4290
  consola.debug("Failed to calculate token count:", error);
4378
4291
  }
4379
4292
  }
4380
- function handleNonStreamingResponse$1(c, originalResponse, ctx, payload) {
4293
+ function handleNonStreamingResponse(c, originalResponse, ctx, payload) {
4381
4294
  consola.debug("Non-streaming response:", JSON.stringify(originalResponse));
4382
4295
  let response = originalResponse;
4383
4296
  if (state.verbose && ctx.truncateResult?.wasCompacted && response.choices[0]?.message.content) {
@@ -4410,17 +4323,19 @@ function handleNonStreamingResponse$1(c, originalResponse, ctx, payload) {
4410
4323
  toolCalls: extractToolCalls(choice)
4411
4324
  }, durationMs);
4412
4325
  const cachedInputTokens = usage ? getCachedTokensFromOpenAIUsage(usage) : void 0;
4326
+ const freshInputTokens = usage ? usage.prompt_tokens - (cachedInputTokens ?? 0) : 0;
4413
4327
  if (ctx.trackingId && usage) requestTracker.updateRequest(ctx.trackingId, {
4414
- inputTokens: usage.prompt_tokens,
4328
+ inputTokens: freshInputTokens,
4415
4329
  outputTokens: usage.completion_tokens,
4416
4330
  queueWaitMs: ctx.queueWaitMs,
4417
4331
  reasoningTokens,
4418
4332
  cachedInputTokens,
4333
+ cacheCreationInputTokens: 0,
4419
4334
  totalInputTokens: usage.prompt_tokens
4420
4335
  });
4421
4336
  captureRequest({
4422
4337
  model: response.model,
4423
- inputTokens: usage?.prompt_tokens ?? 0,
4338
+ inputTokens: freshInputTokens,
4424
4339
  outputTokens: usage?.completion_tokens ?? 0,
4425
4340
  durationMs,
4426
4341
  success: true,
@@ -4428,6 +4343,7 @@ function handleNonStreamingResponse$1(c, originalResponse, ctx, payload) {
4428
4343
  toolCount: payload.tools?.length ?? 0,
4429
4344
  reasoningTokens,
4430
4345
  cachedInputTokens,
4346
+ cacheCreationInputTokens: 0,
4431
4347
  totalInputTokens: usage?.prompt_tokens,
4432
4348
  stopReason: choice.finish_reason
4433
4349
  });
@@ -4467,7 +4383,7 @@ function createStreamAccumulator() {
4467
4383
  toolCallMap: /* @__PURE__ */ new Map()
4468
4384
  };
4469
4385
  }
4470
- async function handleStreamingResponse$1(opts) {
4386
+ async function handleStreamingResponse(opts) {
4471
4387
  const { stream, response, payload, ctx } = opts;
4472
4388
  const acc = createStreamAccumulator();
4473
4389
  const checkRepetition = createStreamRepetitionChecker(`openai:${payload.model}`);
@@ -4497,7 +4413,7 @@ async function handleStreamingResponse$1(opts) {
4497
4413
  await accumulateAndEchoChunk(chunk, acc, checkRepetition, ctx, stream);
4498
4414
  }
4499
4415
  recordStreamSuccess(acc, payload.model, ctx);
4500
- completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens, ctx.queueWaitMs, acc.reasoningTokens, {
4416
+ completeTracking(ctx.trackingId, acc.inputTokens - acc.cachedTokens, acc.outputTokens, ctx.queueWaitMs, acc.reasoningTokens, {
4501
4417
  model: acc.model || payload.model,
4502
4418
  stream: true,
4503
4419
  durationMs: Date.now() - ctx.startTime,
@@ -4505,6 +4421,7 @@ async function handleStreamingResponse$1(opts) {
4505
4421
  toolCount: payload.tools?.length ?? 0
4506
4422
  }, ctx.timings, {
4507
4423
  cachedInputTokens: acc.cachedTokens,
4424
+ cacheCreationInputTokens: 0,
4508
4425
  totalInputTokens: acc.inputTokens
4509
4426
  });
4510
4427
  } catch (error) {
@@ -6600,6 +6517,46 @@ async function checkNeedsCompactionAnthropic(payload, model, config = {}) {
6600
6517
  };
6601
6518
  }
6602
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
+
6603
6560
  //#endregion
6604
6561
  //#region src/lib/anthropic/features.ts
6605
6562
  function normalizeForMatching(modelId) {
@@ -6750,6 +6707,37 @@ function filterServerToolBlocksFromResponse(response) {
6750
6707
  };
6751
6708
  }
6752
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
+
6753
6741
  //#endregion
6754
6742
  //#region src/services/copilot/create-anthropic-messages.ts
6755
6743
  /**
@@ -6952,11 +6940,12 @@ function resolveAnthropicModelForDirectPath(modelId) {
6952
6940
  }
6953
6941
  }
6954
6942
  /**
6955
- * Check if a model supports direct Anthropic API.
6956
- * 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-...).
6957
6947
  */
6958
6948
  function supportsDirectAnthropicApi(modelId) {
6959
- if (state.redirectAnthropic) return false;
6960
6949
  return resolveAnthropicModelForDirectPath(modelId) !== void 0;
6961
6950
  }
6962
6951
 
@@ -7153,15 +7142,6 @@ function extractToolCallsFromContent(content) {
7153
7142
  });
7154
7143
  return tools.length > 0 ? tools : void 0;
7155
7144
  }
7156
- function mapOpenAIStopReasonToAnthropic(finishReason) {
7157
- if (finishReason === null) return null;
7158
- return {
7159
- stop: "end_turn",
7160
- length: "max_tokens",
7161
- tool_calls: "tool_use",
7162
- content_filter: "end_turn"
7163
- }[finishReason];
7164
- }
7165
7145
  function prependMarkerToResponse(response, marker) {
7166
7146
  if (!marker) return response;
7167
7147
  const content = [...response.content];
@@ -7282,549 +7262,73 @@ function recordAnthropicStreamingResponse(acc, fallbackModel, ctx) {
7282
7262
  }
7283
7263
 
7284
7264
  //#endregion
7285
- //#region src/routes/messages/non-stream-translation.ts
7286
- const OPENAI_TOOL_NAME_LIMIT = 64;
7265
+ //#region src/routes/messages/stream-translation.ts
7287
7266
  /**
7288
- * Ensure all tool_use blocks have corresponding tool_result responses.
7289
- * This handles edge cases where conversation history may be incomplete:
7290
- * - Session interruptions where tool execution was cut off
7291
- * - Previous request failures
7292
- * - Client sending truncated history
7267
+ * Wrap an arbitrary error into an Anthropic-native `error` stream event.
7293
7268
  *
7294
- * 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).
7295
7273
  */
7296
- function fixMessageSequence(messages) {
7297
- const fixedMessages = [];
7298
- for (let i = 0; i < messages.length; i++) {
7299
- const message = messages[i];
7300
- fixedMessages.push(message);
7301
- if (message.role === "assistant" && message.tool_calls && message.tool_calls.length > 0) {
7302
- const foundToolResponses = /* @__PURE__ */ new Set();
7303
- let j = i + 1;
7304
- while (j < messages.length && messages[j].role === "tool") {
7305
- const toolMessage = messages[j];
7306
- if (toolMessage.tool_call_id) foundToolResponses.add(toolMessage.tool_call_id);
7307
- j++;
7308
- }
7309
- for (const toolCall of message.tool_calls) if (!foundToolResponses.has(toolCall.id)) {
7310
- consola.debug(`Adding placeholder tool_result for ${toolCall.id}`);
7311
- fixedMessages.push({
7312
- role: "tool",
7313
- tool_call_id: toolCall.id,
7314
- content: "Tool execution was interrupted or failed."
7315
- });
7316
- }
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."
7317
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;
7318
7300
  }
7319
- return fixedMessages;
7301
+ return raw;
7320
7302
  }
7321
- function translateToOpenAI(payload) {
7322
- const toolNameMapping = {
7323
- truncatedToOriginal: /* @__PURE__ */ new Map(),
7324
- originalToTruncated: /* @__PURE__ */ new Map()
7325
- };
7326
- const messages = translateAnthropicMessagesToOpenAI(payload.messages, payload.system, toolNameMapping);
7327
- return {
7328
- payload: {
7329
- model: translateModelName(payload.model),
7330
- messages: fixMessageSequence(messages),
7331
- max_tokens: payload.max_tokens,
7332
- stop: payload.stop_sequences,
7333
- stream: payload.stream,
7334
- temperature: payload.temperature,
7335
- top_p: payload.top_p,
7336
- user: payload.metadata?.user_id,
7337
- tools: translateAnthropicToolsToOpenAI(payload.tools, toolNameMapping),
7338
- tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice, toolNameMapping)
7339
- },
7340
- toolNameMapping
7341
- };
7342
- }
7343
- /**
7344
- * Find the latest available model matching a family prefix.
7345
- * Searches state.models for models starting with the given prefix
7346
- * and returns the one with the highest version number.
7347
- *
7348
- * @param familyPrefix - e.g., "claude-opus", "claude-sonnet", "claude-haiku"
7349
- * @param fallback - fallback model ID if no match found
7350
- */
7351
- function findLatestModel(familyPrefix, fallback) {
7352
- const models = state.models?.data;
7353
- if (!models || models.length === 0) return fallback;
7354
- const candidates = models.filter((m) => m.id.startsWith(familyPrefix));
7355
- if (candidates.length === 0) return fallback;
7356
- candidates.sort((a, b) => {
7357
- const [aMajor, aMinor] = extractVersion(a.id, familyPrefix);
7358
- const [bMajor, bMinor] = extractVersion(b.id, familyPrefix);
7359
- if (aMajor !== bMajor) return bMajor - aMajor;
7360
- return bMinor - aMinor;
7361
- });
7362
- return candidates[0].id;
7363
- }
7364
- /**
7365
- * Extract numeric [major, minor] version from a model id.
7366
- *
7367
- * Supports both naming conventions Anthropic/Copilot have used:
7368
- * - dot: "claude-opus-4.5" → [4, 5]
7369
- * - dash: "claude-opus-4-8" → [4, 8]
7370
- * - dash double-digit: "claude-opus-4-10" → [4, 10]
7371
- *
7372
- * The dash form previously parsed as just the major via the regex
7373
- * /^(\d+(?:\.\d+)?)/ (the dash stopped the match), which silently
7374
- * downgraded dash-named candidates against any dot-named candidate in
7375
- * findLatestModel. Parsing into a tuple also avoids the parseFloat
7376
- * lossiness on double-digit minors ("4.10" → 4.1).
7377
- *
7378
- * Anything after the major/minor segment (date stamps, "-1m") is ignored.
7379
- * The minor capture is bounded to 1-3 digits so that an 8-digit date suffix
7380
- * directly after the major (e.g. "claude-opus-4-20250514") is NOT mistaken
7381
- * for a minor version of 20_250_514 — without that bound, dated ids would
7382
- * outrank legitimate dotted candidates like "claude-opus-4.8" in
7383
- * findLatestModel's sort.
7384
- *
7385
- * Returns [0, 0] when no version can be extracted.
7386
- */
7387
- function extractVersion(modelId, prefix) {
7388
- const match = modelId.slice(prefix.length + 1).match(/^(\d+)(?:[.-](\d{1,3}))?/);
7389
- if (!match) return [0, 0];
7390
- const major = Number.parseInt(match[1], 10);
7391
- const rawMinor = match[2];
7392
- return [major, rawMinor === void 0 ? 0 : Number.parseInt(rawMinor, 10) || 0];
7393
- }
7394
- function translateModelName(model) {
7395
- const aliasMap = {
7396
- opus: "claude-opus",
7397
- sonnet: "claude-sonnet",
7398
- haiku: "claude-haiku"
7399
- };
7400
- if (aliasMap[model]) {
7401
- const familyPrefix = aliasMap[model];
7402
- return findLatestModel(familyPrefix, `${familyPrefix}-4.5`);
7403
- }
7404
- if (/^claude-sonnet-4-5-\d+$/.test(model)) return "claude-sonnet-4.5";
7405
- if (/^claude-sonnet-4-\d+$/.test(model)) return "claude-sonnet-4";
7406
- if (model === "claude-opus-4-8-1m") return "claude-opus-4.8";
7407
- if (model === "claude-opus-4-8") return "claude-opus-4.8";
7408
- if (model === "claude-opus-4-7-1m") return "claude-opus-4.7";
7409
- if (/^claude-opus-4-7$/.test(model)) return "claude-opus-4.7";
7410
- if (model === "claude-opus-4-6-1m") return "claude-opus-4.6-1m";
7411
- if (/^claude-opus-4-6$/.test(model)) return "claude-opus-4.6";
7412
- if (/^claude-opus-4-5-\d+$/.test(model)) return "claude-opus-4.5";
7413
- if (/^claude-opus-4-\d+$/.test(model)) return findLatestModel("claude-opus", "claude-opus-4.5");
7414
- if (/^claude-haiku-4-5-\d+$/.test(model)) return "claude-haiku-4.5";
7415
- if (/^claude-haiku-3-5-\d+$/.test(model)) return findLatestModel("claude-haiku", "claude-haiku-4.5");
7416
- return model;
7417
- }
7418
- function translateAnthropicMessagesToOpenAI(anthropicMessages, system, toolNameMapping) {
7419
- const systemMessages = handleSystemPrompt(system);
7420
- const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message, toolNameMapping));
7421
- return [...systemMessages, ...otherMessages];
7422
- }
7423
- const RESERVED_KEYWORDS = ["x-anthropic-billing-header", "x-anthropic-billing"];
7424
- /**
7425
- * Filter out reserved keywords from system prompt text.
7426
- * Copilot API rejects requests containing these keywords.
7427
- * Removes the entire line containing the keyword to keep the prompt clean.
7428
- */
7429
- function filterReservedKeywords(text) {
7430
- let filtered = text;
7431
- for (const keyword of RESERVED_KEYWORDS) if (text.includes(keyword)) {
7432
- consola.debug(`[Reserved Keyword] Removing line containing "${keyword}"`);
7433
- filtered = filtered.split("\n").filter((line) => !line.includes(keyword)).join("\n");
7434
- }
7435
- return filtered;
7436
- }
7437
- function handleSystemPrompt(system) {
7438
- if (!system) return [];
7439
- if (typeof system === "string") return [{
7440
- role: "system",
7441
- content: filterReservedKeywords(system)
7442
- }];
7443
- else return [{
7444
- role: "system",
7445
- content: filterReservedKeywords(system.map((block) => block.text).join("\n\n"))
7446
- }];
7447
- }
7448
- function handleUserMessage(message) {
7449
- const newMessages = [];
7450
- if (Array.isArray(message.content)) {
7451
- const toolResultBlocks = message.content.filter((block) => block.type === "tool_result");
7452
- const otherBlocks = message.content.filter((block) => block.type !== "tool_result");
7453
- for (const block of toolResultBlocks) newMessages.push({
7454
- role: "tool",
7455
- tool_call_id: block.tool_use_id,
7456
- content: mapContent(block.content)
7457
- });
7458
- if (otherBlocks.length > 0) newMessages.push({
7459
- role: "user",
7460
- content: mapContent(otherBlocks)
7461
- });
7462
- } else newMessages.push({
7463
- role: "user",
7464
- content: mapContent(message.content)
7465
- });
7466
- return newMessages;
7467
- }
7468
- function handleAssistantMessage(message, toolNameMapping) {
7469
- if (!Array.isArray(message.content)) return [{
7470
- role: "assistant",
7471
- content: mapContent(message.content)
7472
- }];
7473
- const toolUseBlocks = message.content.filter((block) => block.type === "tool_use");
7474
- const textBlocks = message.content.filter((block) => block.type === "text");
7475
- const thinkingBlocks = message.content.filter((block) => block.type === "thinking");
7476
- const allTextContent = [...textBlocks.map((b) => b.text), ...thinkingBlocks.map((b) => b.thinking)].join("\n\n");
7477
- return toolUseBlocks.length > 0 ? [{
7478
- role: "assistant",
7479
- content: allTextContent || null,
7480
- tool_calls: toolUseBlocks.map((toolUse) => ({
7481
- id: toolUse.id,
7482
- type: "function",
7483
- function: {
7484
- name: getTruncatedToolName(toolUse.name, toolNameMapping),
7485
- arguments: JSON.stringify(toolUse.input)
7486
- }
7487
- }))
7488
- }] : [{
7489
- role: "assistant",
7490
- content: mapContent(message.content)
7491
- }];
7492
- }
7493
- function mapContent(content) {
7494
- if (typeof content === "string") return content;
7495
- if (!Array.isArray(content)) return null;
7496
- 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");
7497
- const contentParts = [];
7498
- for (const block of content) switch (block.type) {
7499
- case "text":
7500
- contentParts.push({
7501
- type: "text",
7502
- text: block.text
7503
- });
7504
- break;
7505
- case "thinking":
7506
- contentParts.push({
7507
- type: "text",
7508
- text: block.thinking
7509
- });
7510
- break;
7511
- case "image":
7512
- contentParts.push({
7513
- type: "image_url",
7514
- image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
7515
- });
7516
- break;
7517
- }
7518
- return contentParts;
7519
- }
7520
- function getTruncatedToolName(originalName, toolNameMapping) {
7521
- if (originalName.length <= OPENAI_TOOL_NAME_LIMIT) return originalName;
7522
- const existingTruncated = toolNameMapping.originalToTruncated.get(originalName);
7523
- if (existingTruncated) return existingTruncated;
7524
- let hash = 0;
7525
- for (let i = 0; i < originalName.length; i++) {
7526
- const char = originalName.codePointAt(i) ?? 0;
7527
- hash = (hash << 5) - hash + char;
7528
- hash = Math.trunc(hash);
7529
- }
7530
- const hashSuffix = Math.abs(hash).toString(36).slice(0, 8);
7531
- const truncatedName = originalName.slice(0, OPENAI_TOOL_NAME_LIMIT - 9) + "_" + hashSuffix;
7532
- toolNameMapping.truncatedToOriginal.set(truncatedName, originalName);
7533
- toolNameMapping.originalToTruncated.set(originalName, truncatedName);
7534
- consola.debug(`Truncated tool name: "${originalName}" -> "${truncatedName}"`);
7535
- return truncatedName;
7536
- }
7537
- function translateAnthropicToolsToOpenAI(anthropicTools, toolNameMapping) {
7538
- if (!anthropicTools) return;
7539
- return anthropicTools.map((tool) => ({
7540
- type: "function",
7541
- function: {
7542
- name: getTruncatedToolName(tool.name, toolNameMapping),
7543
- description: tool.description,
7544
- parameters: tool.input_schema ?? {}
7545
- }
7546
- }));
7547
- }
7548
- function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice, toolNameMapping) {
7549
- if (!anthropicToolChoice) return;
7550
- switch (anthropicToolChoice.type) {
7551
- case "auto": return "auto";
7552
- case "any": return "required";
7553
- case "tool":
7554
- if (anthropicToolChoice.name) return {
7555
- type: "function",
7556
- function: { name: getTruncatedToolName(anthropicToolChoice.name, toolNameMapping) }
7557
- };
7558
- return;
7559
- case "none": return "none";
7560
- default: return;
7561
- }
7562
- }
7563
- /** Create empty response for edge case of no choices */
7564
- function createEmptyResponse(response) {
7565
- return {
7566
- id: response.id,
7567
- type: "message",
7568
- role: "assistant",
7569
- model: response.model,
7570
- content: [],
7571
- stop_reason: "end_turn",
7572
- stop_sequence: null,
7573
- usage: {
7574
- input_tokens: response.usage?.prompt_tokens ?? 0,
7575
- output_tokens: response.usage?.completion_tokens ?? 0
7576
- }
7577
- };
7578
- }
7579
- /** Build usage object from response */
7580
- function buildUsageObject(response) {
7581
- const cachedTokens = response.usage?.prompt_tokens_details?.cached_tokens;
7582
- return {
7583
- input_tokens: (response.usage?.prompt_tokens ?? 0) - (cachedTokens ?? 0),
7584
- output_tokens: response.usage?.completion_tokens ?? 0,
7585
- ...cachedTokens !== void 0 && { cache_read_input_tokens: cachedTokens }
7586
- };
7587
- }
7588
- function translateToAnthropic(response, toolNameMapping) {
7589
- if (response.choices.length === 0) return createEmptyResponse(response);
7590
- const allTextBlocks = [];
7591
- const allToolUseBlocks = [];
7592
- let stopReason = null;
7593
- stopReason = response.choices[0]?.finish_reason ?? stopReason;
7594
- for (const choice of response.choices) {
7595
- const textBlocks = getAnthropicTextBlocks(choice.message.content);
7596
- const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls, toolNameMapping);
7597
- allTextBlocks.push(...textBlocks);
7598
- allToolUseBlocks.push(...toolUseBlocks);
7599
- if (choice.finish_reason === "tool_calls" || stopReason === "stop") stopReason = choice.finish_reason;
7600
- }
7601
- return {
7602
- id: response.id,
7603
- type: "message",
7604
- role: "assistant",
7605
- model: response.model,
7606
- content: [...allTextBlocks, ...allToolUseBlocks],
7607
- stop_reason: mapOpenAIStopReasonToAnthropic(stopReason),
7608
- stop_sequence: null,
7609
- usage: buildUsageObject(response)
7610
- };
7611
- }
7612
- function getAnthropicTextBlocks(messageContent) {
7613
- if (typeof messageContent === "string") return [{
7614
- type: "text",
7615
- text: messageContent
7616
- }];
7617
- if (Array.isArray(messageContent)) return messageContent.filter((part) => part.type === "text").map((part) => ({
7618
- type: "text",
7619
- text: part.text
7620
- }));
7621
- return [];
7622
- }
7623
- function getAnthropicToolUseBlocks(toolCalls, toolNameMapping) {
7624
- if (!toolCalls) return [];
7625
- return toolCalls.map((toolCall) => {
7626
- let input = {};
7627
- try {
7628
- input = JSON.parse(toolCall.function.arguments);
7629
- } catch (error) {
7630
- consola.warn(`Failed to parse tool call arguments for ${toolCall.function.name}:`, error);
7631
- }
7632
- const originalName = toolNameMapping?.truncatedToOriginal.get(toolCall.function.name) ?? toolCall.function.name;
7633
- return {
7634
- type: "tool_use",
7635
- id: toolCall.id,
7636
- name: originalName,
7637
- input
7638
- };
7639
- });
7640
- }
7641
-
7642
- //#endregion
7643
- //#region src/routes/messages/stream-translation.ts
7644
- function isToolBlockOpen(state) {
7645
- if (!state.contentBlockOpen) return false;
7646
- return Object.values(state.toolCalls).some((tc) => tc.anthropicBlockIndex === state.contentBlockIndex);
7647
- }
7648
- function translateChunkToAnthropicEvents(chunk, state, toolNameMapping) {
7649
- const events = [];
7650
- if (chunk.choices.length === 0) {
7651
- if (chunk.model && !state.model) state.model = chunk.model;
7652
- return events;
7653
- }
7654
- const choice = chunk.choices[0];
7655
- const { delta } = choice;
7656
- if (!state.messageStartSent) {
7657
- const model = chunk.model || state.model || "unknown";
7658
- events.push({
7659
- type: "message_start",
7660
- message: {
7661
- id: chunk.id || `msg_${Date.now()}`,
7662
- type: "message",
7663
- role: "assistant",
7664
- content: [],
7665
- model,
7666
- stop_reason: null,
7667
- stop_sequence: null,
7668
- usage: {
7669
- input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
7670
- output_tokens: 0,
7671
- ...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
7672
- }
7673
- }
7674
- });
7675
- state.messageStartSent = true;
7676
- }
7677
- if (delta.content) {
7678
- if (isToolBlockOpen(state)) {
7679
- events.push({
7680
- type: "content_block_stop",
7681
- index: state.contentBlockIndex
7682
- });
7683
- state.contentBlockIndex++;
7684
- state.contentBlockOpen = false;
7685
- }
7686
- if (!state.contentBlockOpen) {
7687
- events.push({
7688
- type: "content_block_start",
7689
- index: state.contentBlockIndex,
7690
- content_block: {
7691
- type: "text",
7692
- text: ""
7693
- }
7694
- });
7695
- state.contentBlockOpen = true;
7696
- }
7697
- events.push({
7698
- type: "content_block_delta",
7699
- index: state.contentBlockIndex,
7700
- delta: {
7701
- type: "text_delta",
7702
- text: delta.content
7703
- }
7704
- });
7705
- }
7706
- if (delta.tool_calls) for (const toolCall of delta.tool_calls) {
7707
- if (toolCall.id && toolCall.function?.name) {
7708
- if (state.contentBlockOpen) {
7709
- events.push({
7710
- type: "content_block_stop",
7711
- index: state.contentBlockIndex
7712
- });
7713
- state.contentBlockIndex++;
7714
- state.contentBlockOpen = false;
7715
- }
7716
- const originalName = toolNameMapping?.truncatedToOriginal.get(toolCall.function.name) ?? toolCall.function.name;
7717
- const anthropicBlockIndex = state.contentBlockIndex;
7718
- state.toolCalls[toolCall.index] = {
7719
- id: toolCall.id,
7720
- name: originalName,
7721
- anthropicBlockIndex
7722
- };
7723
- events.push({
7724
- type: "content_block_start",
7725
- index: anthropicBlockIndex,
7726
- content_block: {
7727
- type: "tool_use",
7728
- id: toolCall.id,
7729
- name: originalName,
7730
- input: {}
7731
- }
7732
- });
7733
- state.contentBlockOpen = true;
7734
- }
7735
- if (toolCall.function?.arguments) {
7736
- const toolCallInfo = state.toolCalls[toolCall.index];
7737
- if (toolCallInfo) events.push({
7738
- type: "content_block_delta",
7739
- index: toolCallInfo.anthropicBlockIndex,
7740
- delta: {
7741
- type: "input_json_delta",
7742
- partial_json: toolCall.function.arguments
7743
- }
7744
- });
7745
- }
7746
- }
7747
- if (choice.finish_reason) {
7748
- if (state.contentBlockOpen) {
7749
- events.push({
7750
- type: "content_block_stop",
7751
- index: state.contentBlockIndex
7752
- });
7753
- state.contentBlockOpen = false;
7754
- }
7755
- events.push({
7756
- type: "message_delta",
7757
- delta: {
7758
- stop_reason: mapOpenAIStopReasonToAnthropic(choice.finish_reason),
7759
- stop_sequence: null
7760
- },
7761
- usage: {
7762
- input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
7763
- output_tokens: chunk.usage?.completion_tokens ?? 0,
7764
- ...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
7765
- }
7766
- }, { type: "message_stop" });
7767
- }
7768
- return events;
7769
- }
7770
- function translateErrorToAnthropicErrorEvent(error) {
7771
- return {
7772
- type: "error",
7773
- error: {
7774
- type: "api_error",
7775
- message: error ? formatError(error) : "An unexpected error occurred during streaming."
7776
- }
7777
- };
7778
- }
7779
-
7780
- //#endregion
7781
- //#region src/routes/messages/tool-call-recovery.ts
7782
- const ENVELOPE = String.raw`(?:<(?:antml:)?function_calls>|call|count|court)`;
7783
- const INVOKE_BODY = String.raw`<(?:antml:)?invoke\s+name="[^"]+">(?:(?!<(?:antml:)?invoke\b)[\s\S])*?</(?:antml:)?invoke>`;
7784
- 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");
7785
- const STARTS_WITH_ENVELOPE_RE = new RegExp(String.raw`^[ \t\n]*` + ENVELOPE);
7786
- const INVOKE_OPENER_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*)?<(?:antml:)?invoke\b[^\n>]*>?`, "g");
7787
- const LEAK_OPEN_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*)?<(?:antml:)?invoke\s+name="`, "g");
7788
- const INVOKE_RE = /<(?:antml:)?invoke\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?invoke>/g;
7789
- const PARAMETER_RE = /<(?:antml:)?parameter\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?parameter>/g;
7790
- function coerceParamValue(raw) {
7791
- const trimmed = raw.trim();
7792
- if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) try {
7793
- return JSON.parse(trimmed);
7794
- } catch {
7795
- return raw;
7796
- }
7797
- return raw;
7798
- }
7799
- function parseRegionInvokes(region, knownTools) {
7800
- const calls = [];
7801
- for (const invokeMatch of region.matchAll(INVOKE_RE)) {
7802
- const name = invokeMatch[1];
7803
- if (knownTools && !knownTools.has(name)) continue;
7804
- const input = {};
7805
- for (const paramMatch of invokeMatch[2].matchAll(PARAMETER_RE)) input[paramMatch[1]] = coerceParamValue(paramMatch[2]);
7806
- calls.push({
7807
- name,
7808
- input
7809
- });
7810
- }
7811
- return calls;
7812
- }
7813
- const FENCE_DELIM_RE = /(?:^|\n)[ \t]*```/g;
7814
- function insideFence(text, pos) {
7815
- let openAt = -1;
7816
- for (const m of text.matchAll(FENCE_DELIM_RE)) if (openAt === -1) {
7817
- if (m.index >= pos) break;
7818
- openAt = m.index;
7819
- } else if (m.index > pos) return true;
7820
- else openAt = -1;
7821
- return false;
7822
- }
7823
- function regionIsLeak(region, offset, fullText, knownTools) {
7824
- if (insideFence(fullText, offset)) return false;
7825
- if (STARTS_WITH_ENVELOPE_RE.test(region)) return true;
7826
- if (!knownTools) return false;
7827
- return parseRegionInvokes(region, knownTools).length > 0;
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;
7828
7332
  }
7829
7333
  /**
7830
7334
  * Split assistant text into ordered segments — dropping leaked envelope markup
@@ -8462,6 +7966,7 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
8462
7966
  outputTokens: response.usage.output_tokens,
8463
7967
  queueWaitMs: ctx.queueWaitMs,
8464
7968
  cachedInputTokens: cacheRead,
7969
+ cacheCreationInputTokens: cacheCreation,
8465
7970
  totalInputTokens
8466
7971
  });
8467
7972
  captureRequest({
@@ -8473,6 +7978,7 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
8473
7978
  stream: false,
8474
7979
  toolCount: payload.tools?.length ?? 0,
8475
7980
  cachedInputTokens: cacheRead,
7981
+ cacheCreationInputTokens: cacheCreation,
8476
7982
  totalInputTokens,
8477
7983
  stopReason: response.stop_reason ?? void 0
8478
7984
  });
@@ -8561,6 +8067,7 @@ async function handleDirectAnthropicStreamingResponse(opts) {
8561
8067
  toolCount: anthropicPayload.tools?.length ?? 0
8562
8068
  }, ctx.timings, {
8563
8069
  cachedInputTokens: acc.cacheReadInputTokens,
8070
+ cacheCreationInputTokens: acc.cacheCreationInputTokens,
8564
8071
  totalInputTokens: acc.inputTokens + acc.cacheReadInputTokens + acc.cacheCreationInputTokens
8565
8072
  });
8566
8073
  } catch (error) {
@@ -8632,356 +8139,350 @@ const parseSubagentMarkerFromSystemReminder = (text) => {
8632
8139
  };
8633
8140
 
8634
8141
  //#endregion
8635
- //#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
+ }
8636
8199
  /**
8637
- * Handle completion using OpenAI translation path (legacy)
8200
+ * Log tool-related information for debugging
8638
8201
  */
8639
- async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOverride) {
8640
- const originalModelId = anthropicPayload.model;
8641
- const hasOneMillionSuffix = isOneMillionSuffixedClaudeId(originalModelId);
8642
- const oneMResolution = resolveAnthropicModelForDirectPath(originalModelId);
8643
- const needsContext1mBeta = hasOneMillionSuffix || (oneMResolution?.oneMillionFallback ?? false);
8644
- const { payload: translatedPayload, toolNameMapping } = translateToOpenAI(anthropicPayload);
8645
- consola.debug("Translated OpenAI request payload:", JSON.stringify(translatedPayload));
8646
- updateTrackerResolvedModel(ctx.trackingId, translatedPayload.model);
8647
- const selectedModel = findModelById(translatedPayload.model);
8648
- const autoTruncateConfig = oneMResolution?.oneMillionFallback ? {
8649
- contextWindowOverride: oneMResolution.effectiveContextWindowTokens,
8650
- tokenLimitCacheKeyOverride: originalModelId
8651
- } : {};
8652
- const { finalPayload: openAIPayload, truncateResult } = await buildFinalPayload(translatedPayload, selectedModel, autoTruncateConfig);
8653
- if (truncateResult) ctx.truncateResult = truncateResult;
8654
- let anthropicBeta = c.req.header("anthropic-beta");
8655
- if (needsContext1mBeta) anthropicBeta = appendContext1mBeta(anthropicBeta);
8656
- if (state.manualApprove) await awaitApproval();
8657
- let errorModelIdOverride;
8658
- if (autoTruncateConfig.tokenLimitCacheKeyOverride !== void 0) errorModelIdOverride = autoTruncateConfig.tokenLimitCacheKeyOverride;
8659
- else if (needsContext1mBeta && selectedModel) errorModelIdOverride = selectedModel.id;
8660
- else if (hasOneMillionSuffix) errorModelIdOverride = originalModelId;
8661
- const abort = clientAbortController(c);
8662
- const isStreaming = anthropicPayload.stream === true;
8663
- try {
8664
- const settled = settle(executeWithAdaptiveRateLimit(() => createChatCompletions(openAIPayload, {
8665
- initiator: initiatorOverride,
8666
- resolvedModel: selectedModel,
8667
- anthropicBeta,
8668
- errorModelIdOverride,
8669
- signal: abort.signal
8670
- })));
8671
- const raced = isStreaming ? await raceWithGrace(settled, RATE_LIMIT_GRACE_MS) : await settled;
8672
- if (raced.kind === "error") throw raced.error;
8673
- if (raced.kind === "done") {
8674
- const { result: response, queueWaitMs } = raced.value;
8675
- ctx.queueWaitMs = queueWaitMs;
8676
- if (isNonStreaming(response)) return handleNonStreamingResponse({
8677
- c,
8678
- response,
8679
- toolNameMapping,
8680
- ctx,
8681
- anthropicPayload
8682
- });
8683
- consola.debug("Streaming response from Copilot");
8684
- updateTrackerStatus(ctx.trackingId, "streaming");
8685
- return streamSSE(c, async (stream) => {
8686
- stream.onAbort(() => abort.abort());
8687
- await handleStreamingResponse({
8688
- stream,
8689
- response,
8690
- toolNameMapping,
8691
- anthropicPayload,
8692
- 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."
8693
8247
  });
8694
- });
8695
- }
8696
- consola.debug("[RateLimiter] Request queued past grace; opening keepalive stream (translated)");
8697
- updateTrackerStatus(ctx.trackingId, "streaming");
8698
- return streamSSE(c, async (stream) => {
8699
- stream.onAbort(() => abort.abort());
8700
- await runStreamWithKeepalive({
8701
- stream,
8702
- settled,
8703
- pingIntervalMs: KEEPALIVE_PING_INTERVAL_MS,
8704
- onResponse: async ({ result: response, queueWaitMs }) => {
8705
- ctx.queueWaitMs = queueWaitMs;
8706
- if (isNonStreaming(response)) return;
8707
- await handleStreamingResponse({
8708
- stream,
8709
- response,
8710
- toolNameMapping,
8711
- anthropicPayload,
8712
- ctx
8713
- });
8714
- },
8715
- onError: async (error) => {
8716
- if (isAbortError(error)) {
8717
- consola.debug("[Translated] client disconnected during keepalive; upstream aborted");
8718
- failTracking(ctx.trackingId, "client disconnected");
8719
- return;
8720
- }
8721
- recordStreamError({
8722
- acc: createAnthropicStreamAccumulator(),
8723
- fallbackModel: anthropicPayload.model,
8724
- ctx,
8725
- error,
8726
- endpoint: "messages"
8727
- });
8728
- failTracking(ctx.trackingId, error);
8729
- const errorEvent = translateErrorToAnthropicErrorEvent(error);
8730
- await stream.writeSSE({
8731
- event: errorEvent.type,
8732
- data: JSON.stringify(errorEvent)
8733
- });
8734
- }
8735
- });
8736
- });
8737
- } catch (error) {
8738
- if (isAbortError(error)) {
8739
- consola.debug("[Translated] client disconnected before response; upstream aborted");
8740
- failTracking(ctx.trackingId, "client disconnected");
8741
- return new Response(null, { status: 499 });
8248
+ }
8742
8249
  }
8743
- if (error instanceof HTTPError && error.status === 413) await logPayloadSizeInfo(openAIPayload, selectedModel);
8744
- recordErrorResponse(ctx, anthropicPayload.model, error, "messages", anthropicPayload.stream ?? false);
8745
- throw error;
8746
8250
  }
8251
+ return fixedMessages;
8747
8252
  }
8748
- function handleNonStreamingResponse(opts) {
8749
- const { c, response, toolNameMapping, ctx, anthropicPayload } = opts;
8750
- consola.debug("Non-streaming response from Copilot:", JSON.stringify(response).slice(-400));
8751
- let anthropicResponse = translateToAnthropic(response, toolNameMapping);
8752
- consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
8753
- if (state.verbose && ctx.truncateResult?.wasCompacted) {
8754
- const marker = formatClientTruncationMarker(ctx.truncateResult);
8755
- anthropicResponse = prependMarkerToResponse(anthropicResponse, marker);
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;
8287
+ });
8288
+ return candidates[0].id;
8289
+ }
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];
8319
+ }
8320
+ function translateModelName(model) {
8321
+ const aliasMap = {
8322
+ opus: "claude-opus",
8323
+ sonnet: "claude-sonnet",
8324
+ haiku: "claude-haiku"
8325
+ };
8326
+ if (aliasMap[model]) {
8327
+ const familyPrefix = aliasMap[model];
8328
+ return findLatestModel(familyPrefix, `${familyPrefix}-4.5`);
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;
8343
+ }
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];
8348
+ }
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");
8756
8360
  }
8757
- recordResponse(ctx.historyId, {
8758
- success: true,
8759
- model: anthropicResponse.model,
8760
- usage: anthropicResponse.usage,
8761
- stop_reason: anthropicResponse.stop_reason ?? void 0,
8762
- content: {
8763
- role: "assistant",
8764
- content: anthropicResponse.content.map((block) => {
8765
- if (block.type === "text") return {
8766
- type: "text",
8767
- text: block.text
8768
- };
8769
- if (block.type === "tool_use") return {
8770
- type: "tool_use",
8771
- id: block.id,
8772
- name: block.name,
8773
- input: JSON.stringify(block.input)
8774
- };
8775
- return { type: block.type };
8776
- })
8777
- },
8778
- toolCalls: extractToolCallsFromContent(anthropicResponse.content)
8779
- }, Date.now() - ctx.startTime);
8780
- const cacheRead = anthropicResponse.usage.cache_read_input_tokens ?? 0;
8781
- const cacheCreation = anthropicResponse.usage.cache_creation_input_tokens ?? 0;
8782
- const totalInputTokens = anthropicResponse.usage.input_tokens + cacheRead + cacheCreation;
8783
- if (ctx.trackingId) requestTracker.updateRequest(ctx.trackingId, {
8784
- inputTokens: anthropicResponse.usage.input_tokens,
8785
- outputTokens: anthropicResponse.usage.output_tokens,
8786
- queueWaitMs: ctx.queueWaitMs,
8787
- cachedInputTokens: cacheRead,
8788
- totalInputTokens
8789
- });
8790
- captureRequest({
8791
- model: anthropicResponse.model,
8792
- inputTokens: anthropicResponse.usage.input_tokens,
8793
- outputTokens: anthropicResponse.usage.output_tokens,
8794
- durationMs: Date.now() - ctx.startTime,
8795
- success: true,
8796
- stream: false,
8797
- toolCount: anthropicPayload.tools?.length ?? 0,
8798
- cachedInputTokens: cacheRead,
8799
- totalInputTokens,
8800
- stopReason: anthropicResponse.stop_reason ?? void 0
8801
- });
8802
- return c.json(echoResponseBody(anthropicResponse, ctx));
8361
+ return filtered;
8803
8362
  }
8804
- async function handleStreamingResponse(opts) {
8805
- const { stream, response, toolNameMapping, anthropicPayload, ctx } = opts;
8806
- const streamState = {
8807
- messageStartSent: false,
8808
- contentBlockIndex: 0,
8809
- contentBlockOpen: false,
8810
- toolCalls: {}
8811
- };
8812
- const acc = createAnthropicStreamAccumulator();
8813
- const checkRepetition = createStreamRepetitionChecker(`translated:${anthropicPayload.model}`);
8814
- try {
8815
- if (ctx.truncateResult?.wasCompacted) {
8816
- const marker = formatClientTruncationMarker(ctx.truncateResult);
8817
- await sendTruncationMarkerEvent(stream, streamState, marker);
8818
- acc.content += marker;
8819
- }
8820
- await processStreamChunks({
8821
- stream,
8822
- response,
8823
- toolNameMapping,
8824
- streamState,
8825
- acc,
8826
- checkRepetition,
8827
- ctx
8828
- });
8829
- recordAnthropicStreamingResponse(acc, anthropicPayload.model, ctx);
8830
- completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens, ctx.queueWaitMs, void 0, {
8831
- model: acc.model || anthropicPayload.model,
8832
- stream: true,
8833
- durationMs: Date.now() - ctx.startTime,
8834
- stopReason: acc.stopReason || void 0,
8835
- toolCount: anthropicPayload.tools?.length ?? 0
8836
- }, ctx.timings, {
8837
- cachedInputTokens: acc.cacheReadInputTokens,
8838
- totalInputTokens: acc.inputTokens + acc.cacheReadInputTokens + acc.cacheCreationInputTokens
8839
- });
8840
- } catch (error) {
8841
- if (isAbortError(error)) {
8842
- consola.debug("[Translated] client disconnected mid-stream; upstream aborted");
8843
- failTracking(ctx.trackingId, "client disconnected");
8844
- return;
8845
- }
8846
- consola.error("Stream error:", formatError(error));
8847
- recordStreamError({
8848
- acc,
8849
- fallbackModel: anthropicPayload.model,
8850
- ctx,
8851
- error,
8852
- endpoint: "messages"
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)
8853
8383
  });
8854
- failTracking(ctx.trackingId, error);
8855
- const errorEvent = translateErrorToAnthropicErrorEvent(error);
8856
- await stream.writeSSE({
8857
- event: errorEvent.type,
8858
- data: JSON.stringify(errorEvent)
8384
+ if (otherBlocks.length > 0) newMessages.push({
8385
+ role: "user",
8386
+ content: mapContent(otherBlocks)
8859
8387
  });
8860
- }
8861
- }
8862
- async function sendTruncationMarkerEvent(stream, streamState, marker) {
8863
- const blockStartEvent = {
8864
- type: "content_block_start",
8865
- index: streamState.contentBlockIndex,
8866
- content_block: {
8867
- type: "text",
8868
- text: ""
8869
- }
8870
- };
8871
- await stream.writeSSE({
8872
- event: "content_block_start",
8873
- data: JSON.stringify(blockStartEvent)
8874
- });
8875
- const deltaEvent = {
8876
- type: "content_block_delta",
8877
- index: streamState.contentBlockIndex,
8878
- delta: {
8879
- type: "text_delta",
8880
- text: marker
8881
- }
8882
- };
8883
- await stream.writeSSE({
8884
- event: "content_block_delta",
8885
- data: JSON.stringify(deltaEvent)
8886
- });
8887
- const blockStopEvent = {
8888
- type: "content_block_stop",
8889
- index: streamState.contentBlockIndex
8890
- };
8891
- await stream.writeSSE({
8892
- event: "content_block_stop",
8893
- data: JSON.stringify(blockStopEvent)
8388
+ } else newMessages.push({
8389
+ role: "user",
8390
+ content: mapContent(message.content)
8894
8391
  });
8895
- streamState.contentBlockIndex++;
8896
- }
8897
- async function processStreamChunks(opts) {
8898
- const { stream, response, toolNameMapping, streamState, acc, checkRepetition, ctx } = opts;
8899
- for await (const rawEvent of response) {
8900
- consola.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
8901
- if (rawEvent.data === "[DONE]") break;
8902
- if (!rawEvent.data) continue;
8903
- let chunk;
8904
- try {
8905
- chunk = JSON.parse(rawEvent.data);
8906
- } catch (parseError) {
8907
- consola.error("Failed to parse stream chunk:", parseError, rawEvent.data);
8908
- continue;
8909
- }
8910
- if (chunk.model && !acc.model) acc.model = chunk.model;
8911
- const events = translateChunkToAnthropicEvents(chunk, streamState, toolNameMapping);
8912
- for (const event of events) {
8913
- consola.debug("Translated Anthropic event:", JSON.stringify(event));
8914
- processAnthropicEvent(event, acc);
8915
- if (event.type === "content_block_delta" && event.delta.type === "text_delta") checkRepetition(event.delta.text);
8916
- const echoed = echoParsedEvent(event, ctx);
8917
- await stream.writeSSE({
8918
- event: echoed.type,
8919
- data: JSON.stringify(echoed)
8920
- });
8921
- }
8922
- }
8392
+ return newMessages;
8923
8393
  }
8924
-
8925
- //#endregion
8926
- //#region src/routes/messages/handler.ts
8927
- function resolveModelFromBetaHeader(model, betaHeader) {
8928
- if (!betaHeader || !/\bcontext-1m\b/.test(betaHeader)) return model;
8929
- if (!model.startsWith("claude-")) return model;
8930
- if (model.endsWith("-1m")) return model;
8931
- const resolved = `${model}-1m`;
8932
- consola.debug(`Detected context-1m in anthropic-beta header, resolving model: ${model} → ${resolved}`);
8933
- return resolved;
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
+ }];
8934
8418
  }
8935
- async function handleCompletion(c) {
8936
- const rawPayload = await c.req.json();
8937
- consola.debug("Anthropic request payload:", JSON.stringify(rawPayload));
8938
- const { ctx, payload: anthropicPayload } = createEntryContext({
8939
- c,
8940
- rawPayload,
8941
- endpoint: "anthropic",
8942
- normalizePayload: (p) => ({
8943
- ...p,
8944
- model: resolveModelFromBetaHeader(p.model, c.req.header("anthropic-beta"))
8945
- }),
8946
- buildHistoryRequest: (p) => ({
8947
- model: p.model,
8948
- messages: convertAnthropicMessages(p.messages),
8949
- stream: p.stream ?? false,
8950
- tools: p.tools?.map((t) => ({
8951
- name: t.name,
8952
- description: t.description
8953
- })),
8954
- max_tokens: p.max_tokens,
8955
- temperature: p.temperature,
8956
- system: extractSystemPrompt(p.system)
8957
- })
8958
- });
8959
- const sanitizedPayload = dePoisonAssistantMessages(anthropicPayload);
8960
- logToolInfo(sanitizedPayload);
8961
- const subagentMarker = parseSubagentMarkerFromFirstUser(sanitizedPayload);
8962
- const initiatorOverride = subagentMarker ? "agent" : void 0;
8963
- if (subagentMarker) consola.debug("Detected Subagent marker:", JSON.stringify(subagentMarker));
8964
- if (supportsDirectAnthropicApi(sanitizedPayload.model)) {
8965
- normalizeSystemPromptDate(sanitizedPayload);
8966
- injectSystemCacheControl(sanitizedPayload);
8967
- return handleDirectAnthropicCompletion(c, sanitizedPayload, ctx, initiatorOverride);
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;
8968
8443
  }
8969
- return handleTranslatedCompletion(c, sanitizedPayload, ctx, initiatorOverride);
8444
+ return contentParts;
8970
8445
  }
8971
- /**
8972
- * Log tool-related information for debugging
8973
- */
8974
- function logToolInfo(anthropicPayload) {
8975
- if (anthropicPayload.tools?.length) {
8976
- const toolInfo = anthropicPayload.tools.map((t) => ({
8977
- name: t.name,
8978
- type: t.type ?? "(custom)"
8979
- }));
8980
- 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);
8981
8455
  }
8982
- for (const msg of anthropicPayload.messages) if (typeof msg.content !== "string") for (const block of msg.content) {
8983
- if (block.type === "tool_use") consola.debug(`[Tools] tool_use in message: ${block.name} (id: ${block.id})`);
8984
- 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;
8985
8486
  }
8986
8487
  }
8987
8488
 
@@ -9150,6 +8651,26 @@ const createResponses = async (payload, { vision, initiator, resolvedModel, sign
9150
8651
  return await response.json();
9151
8652
  };
9152
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
+
9153
8674
  //#endregion
9154
8675
  //#region src/routes/responses/stream-id-sync.ts
9155
8676
  const createStreamIdTracker = () => ({ outputItems: /* @__PURE__ */ new Map() });
@@ -9388,7 +8909,12 @@ const handleResponses = async (c) => {
9388
8909
  rawPayload,
9389
8910
  endpoint: "openai",
9390
8911
  normalizePayload: (p) => {
9391
- 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;
9392
8918
  useFunctionApplyPatch(np);
9393
8919
  filterUnsupportedBuiltins(np);
9394
8920
  injectPromptCacheKey(np, clientName);
@@ -9464,13 +8990,15 @@ const handleResponses = async (c) => {
9464
8990
  if (finalResult) {
9465
8991
  recordResponseResult(finalResult, model, historyId, startTime);
9466
8992
  const usage = finalResult.usage;
9467
- completeTracking(trackingId, usage?.input_tokens ?? 0, usage?.output_tokens ?? 0, queueWaitMs, usage?.output_tokens_details?.reasoning_tokens, {
8993
+ const cachedInputTokens = usage?.input_tokens_details?.cached_tokens ?? 0;
8994
+ completeTracking(trackingId, (usage?.input_tokens ?? 0) - cachedInputTokens, usage?.output_tokens ?? 0, queueWaitMs, usage?.output_tokens_details?.reasoning_tokens, {
9468
8995
  model: finalResult.model || model,
9469
8996
  stream: true,
9470
8997
  durationMs: Date.now() - startTime,
9471
8998
  toolCount: tools.length
9472
8999
  }, ctx.timings, {
9473
- cachedInputTokens: usage?.input_tokens_details?.cached_tokens ?? 0,
9000
+ cachedInputTokens,
9001
+ cacheCreationInputTokens: 0,
9474
9002
  totalInputTokens: usage?.input_tokens ?? 0
9475
9003
  });
9476
9004
  } else if (streamErrorMessage) {
@@ -9517,14 +9045,17 @@ const handleResponses = async (c) => {
9517
9045
  }
9518
9046
  const result = response;
9519
9047
  const usage = result.usage;
9048
+ const cachedInputTokens = usage?.input_tokens_details?.cached_tokens ?? 0;
9049
+ const freshInputTokens = (usage?.input_tokens ?? 0) - cachedInputTokens;
9520
9050
  recordResponseResult(result, model, historyId, startTime);
9521
- completeTracking(trackingId, usage?.input_tokens ?? 0, usage?.output_tokens ?? 0, ctx.queueWaitMs, usage?.output_tokens_details?.reasoning_tokens, {
9051
+ completeTracking(trackingId, freshInputTokens, usage?.output_tokens ?? 0, ctx.queueWaitMs, usage?.output_tokens_details?.reasoning_tokens, {
9522
9052
  model: result.model || model,
9523
9053
  stream: false,
9524
9054
  durationMs: Date.now() - startTime,
9525
9055
  toolCount: tools.length
9526
9056
  }, ctx.timings, {
9527
- cachedInputTokens: usage?.input_tokens_details?.cached_tokens ?? 0,
9057
+ cachedInputTokens,
9058
+ cacheCreationInputTokens: 0,
9528
9059
  totalInputTokens: usage?.input_tokens ?? 0
9529
9060
  });
9530
9061
  consola.debug("Forwarding native Responses result:", JSON.stringify(result).slice(-400));