@dianshuv/copilot-api 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.mjs +394 -72
  2. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -1348,7 +1348,7 @@ const patchClaude = defineCommand({
1348
1348
 
1349
1349
  //#endregion
1350
1350
  //#region package.json
1351
- var version = "0.8.0";
1351
+ var version = "0.9.0";
1352
1352
 
1353
1353
  //#endregion
1354
1354
  //#region src/lib/adaptive-rate-limiter.ts
@@ -3042,6 +3042,158 @@ const awaitApproval = async () => {
3042
3042
  if (!await consola.prompt(`Accept incoming request?`, { type: "confirm" })) throw new HTTPError("Request rejected", 403, JSON.stringify({ message: "Request rejected" }));
3043
3043
  };
3044
3044
 
3045
+ //#endregion
3046
+ //#region src/lib/echo-model.ts
3047
+ /**
3048
+ * Capture the requested model id from a raw request `model` value, classifying
3049
+ * it into the three-state contract. `undefined`/missing → `absent`; `""` →
3050
+ * `empty`; any other string → `present`.
3051
+ */
3052
+ function captureRequestedModel(rawModel) {
3053
+ if (rawModel === void 0 || rawModel === null) return { kind: "absent" };
3054
+ if (typeof rawModel !== "string") return { kind: "absent" };
3055
+ if (rawModel === "") return { kind: "empty" };
3056
+ return {
3057
+ kind: "present",
3058
+ value: rawModel
3059
+ };
3060
+ }
3061
+ /**
3062
+ * Resolve a {@link RequestedModel} into the action to take on a model field:
3063
+ * - `{ write: true, value }` → set the field to `value`.
3064
+ * - `{ write: false, omit: true }` → remove the field (absent case).
3065
+ * - `null` → leave the field untouched (context-missing case).
3066
+ */
3067
+ function resolveFieldAction(requested) {
3068
+ if (requested.kind === "present") return { value: requested.value };
3069
+ if (requested.kind === "empty") return { value: "" };
3070
+ if (requested.kind === "absent") return { omit: true };
3071
+ return null;
3072
+ }
3073
+ /**
3074
+ * Apply the requested-model action to a single `model`-like key on a shallow
3075
+ * clone of `obj`. Returns a new object; never mutates `obj`. If `obj` does not
3076
+ * own `key`, it is returned (cloned) unchanged regardless of the action — we
3077
+ * only ever rewrite a field the upstream payload actually carries (supports
3078
+ * AC-MALFORMED-SSE: no model field → original passthrough).
3079
+ */
3080
+ function rewriteKey(obj, key, requested) {
3081
+ if (!Object.hasOwn(obj, key)) return obj;
3082
+ const action = resolveFieldAction(requested);
3083
+ if (action === null) return obj;
3084
+ if ("omit" in action) {
3085
+ const { [key]: _omitted, ...rest } = obj;
3086
+ return rest;
3087
+ }
3088
+ return {
3089
+ ...obj,
3090
+ [key]: action.value
3091
+ };
3092
+ }
3093
+ /**
3094
+ * Core rewrite over a plain record. Rewrites the documented model fields:
3095
+ * - top-level `model`, top-level `modelVersion` (Gemini),
3096
+ * - nested `message.model` (Anthropic message_start),
3097
+ * - nested `response.model` (OpenAI Responses event).
3098
+ * Returns a shallow clone; never mutates the input.
3099
+ */
3100
+ function echoRecord(body, requested) {
3101
+ let out = rewriteKey(body, "model", requested);
3102
+ out = rewriteKey(out, "modelVersion", requested);
3103
+ if (isRecord$1(out.message) && Object.hasOwn(out.message, "model")) {
3104
+ const newMessage = rewriteKey(out.message, "model", requested);
3105
+ if (newMessage !== out.message) out = {
3106
+ ...out,
3107
+ message: newMessage
3108
+ };
3109
+ }
3110
+ if (isRecord$1(out.response) && Object.hasOwn(out.response, "model")) {
3111
+ const newResponse = rewriteKey(out.response, "model", requested);
3112
+ if (newResponse !== out.response) out = {
3113
+ ...out,
3114
+ response: newResponse
3115
+ };
3116
+ }
3117
+ return out;
3118
+ }
3119
+ /**
3120
+ * Rewrite the documented client-facing model field(s) of a JSON response body
3121
+ * to the requested model id. Handles every supported non-stream/body shape:
3122
+ * - top-level `model` (OpenAI chat/completions & Responses bodies, Anthropic
3123
+ * messages body, embeddings),
3124
+ * - nested `message.model` (Anthropic `message_start` event object),
3125
+ * - nested `response.model` (OpenAI Responses streaming event object),
3126
+ * - top-level `modelVersion` (Gemini body & chunk).
3127
+ *
3128
+ * Returns a shallow-cloned object; the input is never mutated. Fields that are
3129
+ * not present are left as-is (a payload with no model field round-trips
3130
+ * unchanged), supporting AC-MALFORMED-SSE.
3131
+ *
3132
+ * The generic is constrained to `object` (not an index-signature shape) so it
3133
+ * accepts the project's domain interfaces (`AnthropicResponse`,
3134
+ * `ChatCompletionChunk`, …) directly without forcing callers to widen them.
3135
+ */
3136
+ function echoModelInResponseBody(body, requested) {
3137
+ return echoRecord(body, requested);
3138
+ }
3139
+ /**
3140
+ * Ensure a response body's top-level `model` field equals the requested model
3141
+ * id — **setting it even when the body omits it**. This differs from
3142
+ * {@link echoModelInResponseBody}, which only rewrites a `model` field the
3143
+ * payload already carries (the passthrough rule that protects malformed-SSE /
3144
+ * model-less events). Some upstreams omit `model` from an otherwise-valid
3145
+ * response body (notably the Copilot embeddings endpoint, whose 200 body carries
3146
+ * only `data` + `usage`); for those, the documented client-facing contract is
3147
+ * still "`response.model` == R", so the field must be added, not skipped.
3148
+ *
3149
+ * Three-state per AC-MISSING-MODEL, keyed on the REQUESTER's model (not the
3150
+ * upstream's):
3151
+ * - `present` → set `model` to R (added if absent, overwritten if present).
3152
+ * - `empty` → set `model` to "" (the client sent an empty string).
3153
+ * - `absent` → omit `model` (client sent no model → never invent one); if the
3154
+ * body happened to carry an upstream `model`, drop it.
3155
+ * - `context-missing` → leave the body untouched (defensive passthrough).
3156
+ *
3157
+ * Only the documented client's own string R is ever written — never the upstream
3158
+ * id — so this introduces no side channel (AC-NO-SIDECHANNEL). Returns a shallow
3159
+ * clone; the input is never mutated (protects the AC-OBS data source).
3160
+ */
3161
+ function echoTopLevelModel(body, requested) {
3162
+ if (requested.kind === "context-missing") return body;
3163
+ const rec = body;
3164
+ if (requested.kind === "absent") {
3165
+ if (!Object.hasOwn(rec, "model")) return body;
3166
+ const { model: _dropped, ...rest } = rec;
3167
+ return rest;
3168
+ }
3169
+ const value = requested.kind === "present" ? requested.value : "";
3170
+ return {
3171
+ ...rec,
3172
+ model: value
3173
+ };
3174
+ }
3175
+ function isRecord$1(value) {
3176
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3177
+ }
3178
+ /**
3179
+ * Rewrite the model field of an already-parsed SSE event payload to the
3180
+ * requested model id, dispatched by protocol shape:
3181
+ * - Anthropic `message_start` → `message.model`,
3182
+ * - OpenAI chunk → top-level `model`,
3183
+ * - OpenAI Responses event → nested `response.model`,
3184
+ * - Gemini chunk → `modelVersion`.
3185
+ *
3186
+ * The field dispatch is identical to {@link echoModelInResponseBody} (both
3187
+ * operate on the same documented model fields), so this delegates to the same
3188
+ * core rather than duplicating the shape logic — the two exports exist to name
3189
+ * the two responsibilities (body vs parsed-event) at call sites, per the PRD's
3190
+ * single-policy-point design. Events with no model field round-trip unchanged
3191
+ * (AC-MALFORMED-SSE); the input is never mutated.
3192
+ */
3193
+ function echoModelInParsedEvent(event, requested) {
3194
+ return echoRecord(event, requested);
3195
+ }
3196
+
3045
3197
  //#endregion
3046
3198
  //#region src/lib/message-sanitizer.ts
3047
3199
  const startPattern = /^\s*<system-reminder>[\s\S]*?<\/system-reminder>\n*/;
@@ -3448,13 +3600,39 @@ function isOneMillionSuffixedClaudeId(modelId) {
3448
3600
  }
3449
3601
 
3450
3602
  //#endregion
3451
- //#region src/services/copilot/create-chat-completions.ts
3452
- const GPT_MODEL_PATTERN = /^gpt-/i;
3603
+ //#region src/lib/headers.ts
3604
+ /**
3605
+ * Vendor-neutral header-bag helpers.
3606
+ *
3607
+ * HTTP header names are case-insensitive, but a plain-object header bag is
3608
+ * case-sensitive on its keys. Code that wants to look up "anthropic-beta"
3609
+ * without knowing whether some other producer wrote "Anthropic-Beta" needs
3610
+ * `findHeaderKey`. Code that wants to set a header without creating a
3611
+ * second case variant of the same name needs `setHeader`.
3612
+ */
3453
3613
  /** Case-insensitive lookup of a header key in a plain-object header bag. */
3454
3614
  function findHeaderKey(headers, name) {
3455
3615
  const lower = name.toLowerCase();
3456
3616
  return Object.keys(headers).find((k) => k.toLowerCase() === lower);
3457
3617
  }
3618
+ /** Case-insensitive read of a header value. */
3619
+ function getHeader(headers, name) {
3620
+ const key = findHeaderKey(headers, name);
3621
+ return key === void 0 ? void 0 : headers[key];
3622
+ }
3623
+ /**
3624
+ * Set a header value at the existing case variant if one is present, else at
3625
+ * the supplied canonical name. Prevents a second key (different case) from
3626
+ * being added for the same logical header.
3627
+ */
3628
+ function setHeader(headers, name, value) {
3629
+ const key = findHeaderKey(headers, name) ?? name;
3630
+ headers[key] = value;
3631
+ }
3632
+
3633
+ //#endregion
3634
+ //#region src/services/copilot/create-chat-completions.ts
3635
+ const GPT_MODEL_PATTERN = /^gpt-/i;
3458
3636
  const createChatCompletions = async (payload, options) => {
3459
3637
  if (!state.copilotToken) throw new Error("Copilot token not found");
3460
3638
  const vendor = options?.resolvedModel?.vendor;
@@ -3887,6 +4065,32 @@ function extractErrorMetrics(error) {
3887
4065
  * Shared utilities for request handlers.
3888
4066
  * Contains common functions used by both OpenAI and Anthropic message handlers.
3889
4067
  */
4068
+ /**
4069
+ * Resolve the requested model id (R) carried on the response context into a
4070
+ * {@link RequestedModel}. A context that never captured R (`undefined`) is
4071
+ * treated as `context-missing` so the echo passes the upstream value through
4072
+ * unchanged — it must never invent an id or throw.
4073
+ */
4074
+ function requestedModelOf(ctx) {
4075
+ return ctx.requestedModel ?? { kind: "context-missing" };
4076
+ }
4077
+ /**
4078
+ * Echo the requested model id into a JSON response body at the client write-out
4079
+ * boundary. Thin context-aware wrapper over {@link echoModelInResponseBody};
4080
+ * MUST be called AFTER history/posthog/TUI have read the upstream value.
4081
+ */
4082
+ function echoResponseBody(body, ctx) {
4083
+ return echoModelInResponseBody(body, requestedModelOf(ctx));
4084
+ }
4085
+ /**
4086
+ * Echo the requested model id into an already-parsed SSE event payload at the
4087
+ * client write-out boundary. Thin context-aware wrapper over
4088
+ * {@link echoModelInParsedEvent}; MUST be called AFTER the stream accumulator
4089
+ * (the observability data source) has read the upstream value.
4090
+ */
4091
+ function echoParsedEvent(event, ctx) {
4092
+ return echoModelInParsedEvent(event, requestedModelOf(ctx));
4093
+ }
3890
4094
  /** Helper to update tracker model */
3891
4095
  function updateTrackerModel(trackingId, model, resolvedModel) {
3892
4096
  if (!trackingId) return;
@@ -4115,6 +4319,7 @@ function getReasoningTokensFromOpenAIUsage(usage) {
4115
4319
  async function handleCompletion$1(c) {
4116
4320
  const originalPayload = await c.req.json();
4117
4321
  consola.debug("Request payload:", JSON.stringify(originalPayload).slice(-400));
4322
+ const requestedModel = captureRequestedModel(originalPayload.model);
4118
4323
  const trackingId = c.get("trackingId");
4119
4324
  const startTime = (trackingId ? requestTracker.getRequest(trackingId) : void 0)?.startTime ?? Date.now();
4120
4325
  updateTrackerModel(trackingId, originalPayload.model);
@@ -4131,7 +4336,8 @@ async function handleCompletion$1(c) {
4131
4336
  temperature: originalPayload.temperature ?? void 0
4132
4337
  }),
4133
4338
  trackingId,
4134
- startTime
4339
+ startTime,
4340
+ requestedModel
4135
4341
  };
4136
4342
  const selectedModel = findModelById(originalPayload.model);
4137
4343
  await logTokenCount(originalPayload, selectedModel);
@@ -4236,7 +4442,7 @@ function handleNonStreamingResponse$1(c, originalResponse, ctx, payload) {
4236
4442
  reasoningTokens,
4237
4443
  stopReason: choice.finish_reason
4238
4444
  });
4239
- return c.json(response);
4445
+ return c.json(echoResponseBody(response, ctx));
4240
4446
  }
4241
4447
  function buildResponseContent(choice) {
4242
4448
  return {
@@ -4291,15 +4497,14 @@ async function handleStreamingResponse$1(opts) {
4291
4497
  }]
4292
4498
  };
4293
4499
  await stream.writeSSE({
4294
- data: JSON.stringify(markerChunk),
4500
+ data: JSON.stringify(echoParsedEvent(markerChunk, ctx)),
4295
4501
  event: "message"
4296
4502
  });
4297
4503
  acc.content += marker;
4298
4504
  }
4299
4505
  for await (const chunk of response) {
4300
4506
  consola.debug("Streaming chunk:", JSON.stringify(chunk));
4301
- parseStreamChunk(chunk, acc, checkRepetition);
4302
- await stream.writeSSE(chunk);
4507
+ await accumulateAndEchoChunk(chunk, acc, checkRepetition, ctx, stream);
4303
4508
  }
4304
4509
  recordStreamSuccess(acc, payload.model, ctx);
4305
4510
  completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens, ctx.queueWaitMs, acc.reasoningTokens, {
@@ -4332,45 +4537,70 @@ async function handleStreamingResponse$1(opts) {
4332
4537
  }]
4333
4538
  };
4334
4539
  await stream.writeSSE({
4335
- data: JSON.stringify(markerChunk),
4540
+ data: JSON.stringify(echoParsedEvent(markerChunk, ctx)),
4336
4541
  event: "message"
4337
4542
  });
4338
4543
  } catch {}
4339
4544
  }
4340
4545
  }
4341
- function parseStreamChunk(chunk, acc, checkRepetition) {
4342
- if (!chunk.data || chunk.data === "[DONE]") return;
4546
+ async function accumulateAndEchoChunk(chunk, acc, checkRepetition, ctx, stream) {
4547
+ if (!chunk.data || chunk.data === "[DONE]") {
4548
+ await stream.writeSSE(chunk);
4549
+ return;
4550
+ }
4551
+ let parsed;
4343
4552
  try {
4344
- const parsed = JSON.parse(chunk.data);
4345
- if (parsed.model && !acc.model) acc.model = parsed.model;
4346
- if (parsed.usage) {
4347
- acc.inputTokens = parsed.usage.prompt_tokens;
4348
- acc.outputTokens = parsed.usage.completion_tokens;
4349
- acc.reasoningTokens = getReasoningTokensFromOpenAIUsage(parsed.usage) ?? 0;
4350
- }
4351
- const choice = parsed.choices[0];
4352
- if (choice) {
4353
- if (choice.delta.content) {
4354
- acc.content += choice.delta.content;
4355
- checkRepetition(choice.delta.content);
4356
- }
4357
- if (choice.delta.tool_calls) for (const tc of choice.delta.tool_calls) {
4358
- const idx = tc.index;
4359
- if (!acc.toolCallMap.has(idx)) acc.toolCallMap.set(idx, {
4360
- id: tc.id ?? "",
4361
- name: tc.function?.name ?? "",
4362
- arguments: ""
4363
- });
4364
- const item = acc.toolCallMap.get(idx);
4365
- if (item) {
4366
- if (tc.id) item.id = tc.id;
4367
- if (tc.function?.name) item.name = tc.function.name;
4368
- if (tc.function?.arguments) item.arguments += tc.function.arguments;
4369
- }
4553
+ parsed = JSON.parse(chunk.data);
4554
+ } catch {
4555
+ await stream.writeSSE(chunk);
4556
+ return;
4557
+ }
4558
+ if (typeof parsed !== "object" || parsed === null) {
4559
+ await stream.writeSSE(chunk);
4560
+ return;
4561
+ }
4562
+ try {
4563
+ accumulateParsedChunk(parsed, acc, checkRepetition);
4564
+ } catch {}
4565
+ if (!Object.hasOwn(parsed, "model")) {
4566
+ await stream.writeSSE(chunk);
4567
+ return;
4568
+ }
4569
+ const echoed = echoParsedEvent(parsed, ctx);
4570
+ await stream.writeSSE({
4571
+ ...chunk,
4572
+ data: JSON.stringify(echoed)
4573
+ });
4574
+ }
4575
+ function accumulateParsedChunk(parsed, acc, checkRepetition) {
4576
+ if (parsed.model && !acc.model) acc.model = parsed.model;
4577
+ if (parsed.usage) {
4578
+ acc.inputTokens = parsed.usage.prompt_tokens;
4579
+ acc.outputTokens = parsed.usage.completion_tokens;
4580
+ acc.reasoningTokens = getReasoningTokensFromOpenAIUsage(parsed.usage) ?? 0;
4581
+ }
4582
+ const choice = parsed.choices?.[0];
4583
+ if (choice) {
4584
+ if (choice.delta?.content) {
4585
+ acc.content += choice.delta.content;
4586
+ checkRepetition(choice.delta.content);
4587
+ }
4588
+ if (choice.delta?.tool_calls) for (const tc of choice.delta.tool_calls) {
4589
+ const idx = tc.index;
4590
+ if (!acc.toolCallMap.has(idx)) acc.toolCallMap.set(idx, {
4591
+ id: tc.id ?? "",
4592
+ name: tc.function?.name ?? "",
4593
+ arguments: ""
4594
+ });
4595
+ const item = acc.toolCallMap.get(idx);
4596
+ if (item) {
4597
+ if (tc.id) item.id = tc.id;
4598
+ if (tc.function?.name) item.name = tc.function.name;
4599
+ if (tc.function?.arguments) item.arguments += tc.function.arguments;
4370
4600
  }
4371
- if (choice.finish_reason) acc.finishReason = choice.finish_reason;
4372
4601
  }
4373
- } catch {}
4602
+ if (choice.finish_reason) acc.finishReason = choice.finish_reason;
4603
+ }
4374
4604
  }
4375
4605
  function recordStreamSuccess(acc, fallbackModel, ctx) {
4376
4606
  for (const tc of acc.toolCallMap.values()) if (tc.id && tc.name) acc.toolCalls.push(tc);
@@ -4450,10 +4680,30 @@ const createEmbeddings = async (payload) => {
4450
4680
  //#endregion
4451
4681
  //#region src/routes/embeddings/route.ts
4452
4682
  const embeddingRoutes = new Hono();
4683
+ function isRecord(value) {
4684
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4685
+ }
4453
4686
  embeddingRoutes.post("/", async (c) => {
4687
+ const startTime = Date.now();
4454
4688
  try {
4455
- const response = await createEmbeddings(await c.req.json());
4456
- return c.json(response);
4689
+ const rawBody = await c.req.json();
4690
+ const payload = rawBody;
4691
+ const requestedModel = captureRequestedModel(isRecord(rawBody) ? rawBody.model : void 0);
4692
+ const response = await createEmbeddings(payload);
4693
+ if (!isRecord(response)) return c.json(response);
4694
+ const upstreamModel = typeof response.model === "string" ? response.model : "";
4695
+ const usage = response.usage;
4696
+ captureRequest({
4697
+ model: upstreamModel,
4698
+ inputTokens: isRecord(usage) && typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0,
4699
+ outputTokens: 0,
4700
+ durationMs: Date.now() - startTime,
4701
+ success: true,
4702
+ stream: false,
4703
+ toolCount: 0,
4704
+ endpoint: "embeddings"
4705
+ });
4706
+ return c.json(echoTopLevelModel(response, requestedModel));
4457
4707
  } catch (error) {
4458
4708
  return forwardError(c, error);
4459
4709
  }
@@ -4805,7 +5055,7 @@ function buildUsageMetadata(usage) {
4805
5055
 
4806
5056
  //#endregion
4807
5057
  //#region src/routes/gemini/handler.ts
4808
- async function handleGeminiGenerate(c, model, isStream) {
5058
+ async function handleGeminiGenerate(c, model, isStream, requestedModel) {
4809
5059
  try {
4810
5060
  const geminiRequest = await c.req.json();
4811
5061
  consola.debug("Gemini request for model:", model, "stream:", isStream);
@@ -4830,7 +5080,8 @@ async function handleGeminiGenerate(c, model, isStream) {
4830
5080
  temperature: payload.temperature ?? void 0
4831
5081
  }),
4832
5082
  trackingId,
4833
- startTime
5083
+ startTime,
5084
+ requestedModel
4834
5085
  };
4835
5086
  const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createChatCompletions(payload, { resolvedModel: selectedModel }));
4836
5087
  ctx.queueWaitMs = queueWaitMs;
@@ -4853,7 +5104,7 @@ async function handleGeminiGenerate(c, model, isStream) {
4853
5104
  continue;
4854
5105
  }
4855
5106
  const geminiChunks = translateOpenAIChunkToGemini(chunk, streamState);
4856
- for (const gc of geminiChunks) await s.write(`data: ${JSON.stringify(gc)}\n\n`);
5107
+ for (const gc of geminiChunks) await s.write(`data: ${JSON.stringify(echoParsedEvent(gc, ctx))}\n\n`);
4857
5108
  }
4858
5109
  recordResponse(ctx.historyId, {
4859
5110
  success: true,
@@ -4921,7 +5172,7 @@ function handleNonStreamResponse(c, response, model, ctx, payload) {
4921
5172
  stopReason: response.choices[0]?.finish_reason,
4922
5173
  toolCount: payload.tools?.length ?? 0
4923
5174
  });
4924
- return c.json(geminiResponse);
5175
+ return c.json(echoResponseBody(geminiResponse, ctx));
4925
5176
  }
4926
5177
 
4927
5178
  //#endregion
@@ -4957,11 +5208,13 @@ geminiRoutes.post("/:modelAction", async (c) => {
4957
5208
  const modelAction = c.req.param("modelAction");
4958
5209
  const colonIndex = modelAction.lastIndexOf(":");
4959
5210
  if (colonIndex === -1) return geminiError(c, 400, "INVALID_ARGUMENT", "Missing action in URL");
4960
- const model = resolveGeminiModelAlias(modelAction.slice(0, Math.max(0, colonIndex)));
5211
+ const rawModel = modelAction.slice(0, Math.max(0, colonIndex));
5212
+ const requestedModel = captureRequestedModel(rawModel);
5213
+ const model = resolveGeminiModelAlias(rawModel);
4961
5214
  const action = modelAction.slice(Math.max(0, colonIndex + 1));
4962
5215
  switch (action) {
4963
- case "generateContent": return handleGeminiGenerate(c, model, false);
4964
- case "streamGenerateContent": return handleGeminiGenerate(c, model, true);
5216
+ case "generateContent": return handleGeminiGenerate(c, model, false, requestedModel);
5217
+ case "streamGenerateContent": return handleGeminiGenerate(c, model, true, requestedModel);
4965
5218
  case "countTokens": return handleGeminiCountTokens(c, model);
4966
5219
  default: return geminiError(c, 400, "INVALID_ARGUMENT", `Unknown action: ${action}`);
4967
5220
  }
@@ -7121,15 +7374,18 @@ async function createAnthropicMessages(payload, options) {
7121
7374
  const headers = {
7122
7375
  ...copilotHeaders(state, {
7123
7376
  vision: enableVision,
7124
- intent: isAgentCall ? "conversation-agent" : "conversation-panel"
7377
+ intent: isAgentCall ? "conversation-agent" : "conversation-panel",
7378
+ modelRequestHeaders: resolvedModel?.request_headers
7125
7379
  }),
7126
7380
  "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user"),
7127
7381
  "anthropic-version": "2023-06-01"
7128
7382
  };
7129
- const betaHeaders = buildAnthropicBetaHeaders(filteredPayload.model, resolvedModel);
7130
- Object.assign(headers, betaHeaders);
7131
- if (options?.injectContext1mBeta) headers["anthropic-beta"] = appendContext1mBeta(headers["anthropic-beta"]);
7132
- if (isContextEditingEnabled(filteredPayload.model)) {
7383
+ const proxyBeta = buildAnthropicBetaHeaders(filteredPayload.model, resolvedModel)["anthropic-beta"];
7384
+ let mergedBeta = mergeBetaFeatures(getHeader(headers, "anthropic-beta"), proxyBeta);
7385
+ if (options?.injectContext1mBeta) mergedBeta = appendContext1mBeta(mergedBeta);
7386
+ if (options?.clientAnthropicBetaHeader) mergedBeta = mergeBetaFeatures(mergedBeta, options.clientAnthropicBetaHeader);
7387
+ if (mergedBeta.length > 0) setHeader(headers, "anthropic-beta", mergedBeta);
7388
+ if (!("context_management" in filteredPayload) && isContextEditingEnabled(filteredPayload.model)) {
7133
7389
  const hasThinking = filteredPayload.thinking?.type === "enabled";
7134
7390
  const cm = buildContextManagement(state.contextEditingMode, hasThinking);
7135
7391
  if (cm) {
@@ -7178,7 +7434,11 @@ function stripServerToolsFromPayload(tools) {
7178
7434
  }
7179
7435
  return result.length > 0 ? result : void 0;
7180
7436
  }
7181
- /** Context window unlocked by the context-1m-2025-08-07 beta header. */
7437
+ /**
7438
+ * Effective 1M context window. Two ways a request lands on this size:
7439
+ * (a) base model id + context-1m-2025-08-07 beta header (e.g. claude-opus-4.8),
7440
+ * (b) a distinct upstream "-1m-internal" model id (e.g. claude-opus-4.7-1m-internal).
7441
+ */
7182
7442
  const ONE_MILLION_CONTEXT_WINDOW_TOKENS = 1e6;
7183
7443
  /**
7184
7444
  * Convert a Claude model id from the client-facing dash convention to the
@@ -7212,7 +7472,17 @@ function resolveAnthropicModelForDirectPath(modelId) {
7212
7472
  }
7213
7473
  if (modelId.endsWith("-1m")) {
7214
7474
  const baseId = modelId.slice(0, -3);
7215
- for (const candidateId of [baseId, dashToDotClaudeId(baseId)]) {
7475
+ const dottedBaseId = dashToDotClaudeId(baseId);
7476
+ for (const candidateId of [`${baseId}-1m-internal`, `${dottedBaseId}-1m-internal`]) {
7477
+ const internal = findModelById(candidateId);
7478
+ if (internal?.vendor === "Anthropic") return {
7479
+ model: internal,
7480
+ baseModelId: candidateId,
7481
+ oneMillionFallback: false,
7482
+ effectiveContextWindowTokens: ONE_MILLION_CONTEXT_WINDOW_TOKENS
7483
+ };
7484
+ }
7485
+ for (const candidateId of [baseId, dottedBaseId]) {
7216
7486
  const base = findModelById(candidateId);
7217
7487
  if (base?.vendor === "Anthropic") return {
7218
7488
  model: base,
@@ -7928,11 +8198,13 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
7928
8198
  }
7929
8199
  } else if (state.autoTruncate && !selectedModel) consola.debug(`[Anthropic] Model '${anthropicPayload.model}' not found, skipping auto-truncate`);
7930
8200
  if (state.manualApprove) await awaitApproval();
8201
+ const clientAnthropicBetaHeader = c.req.header("anthropic-beta");
7931
8202
  try {
7932
8203
  const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createAnthropicMessages(effectivePayload, {
7933
8204
  initiator: initiatorOverride,
7934
8205
  injectContext1mBeta: needsContext1mBeta,
7935
- errorModelIdOverride: needsContext1mBeta ? originalModelId : void 0
8206
+ errorModelIdOverride: needsContext1mBeta ? originalModelId : void 0,
8207
+ clientAnthropicBetaHeader
7936
8208
  }));
7937
8209
  ctx.queueWaitMs = queueWaitMs;
7938
8210
  if (Symbol.asyncIterator in response) {
@@ -8022,7 +8294,40 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
8022
8294
  if (state.verbose && truncateResult?.wasCompacted) finalResponse = prependMarkerToResponse(response, createTruncationMarker$1(truncateResult));
8023
8295
  logServerToolBlocks(finalResponse.content);
8024
8296
  finalResponse = filterServerToolBlocksFromResponse(finalResponse);
8025
- return c.json(finalResponse);
8297
+ return c.json(echoResponseBody(finalResponse, ctx));
8298
+ }
8299
+ /**
8300
+ * Echo the requester's original model id into a single already-serialized
8301
+ * native-Anthropic SSE `data:` payload at the write-out boundary.
8302
+ *
8303
+ * In the native Anthropic stream only `message_start` carries a model field
8304
+ * (`message.model`), so every other event type is forwarded byte-for-byte —
8305
+ * this both avoids a needless re-parse per text delta and guarantees events
8306
+ * with no model field round-trip unchanged. For `message_start`, the payload
8307
+ * is parsed and run through the shared echo policy point; the re-serialized
8308
+ * form is returned ONLY when the echo actually rewrote the object (identity
8309
+ * change). When nothing was rewritten — a model-less `message_start`, or
8310
+ * `context-missing` R — the original `forwardData` bytes are returned verbatim
8311
+ * so a passthrough never re-minifies / re-orders the upstream frame. If the
8312
+ * payload fails to parse (malformed upstream frame), the original string is
8313
+ * likewise returned untouched so the stream is never corrupted or interrupted
8314
+ * (AC-MALFORMED-SSE).
8315
+ *
8316
+ * Operates on the serialized output of the server-tool rewrite (not the raw
8317
+ * upstream frame), so the echo composes with any index remap that step made.
8318
+ */
8319
+ function echoForwardData(forwardData, eventType, ctx) {
8320
+ if (eventType !== "message_start") return forwardData;
8321
+ let parsed;
8322
+ try {
8323
+ parsed = JSON.parse(forwardData);
8324
+ } catch {
8325
+ return forwardData;
8326
+ }
8327
+ if (typeof parsed !== "object" || parsed === null) return forwardData;
8328
+ const echoed = echoParsedEvent(parsed, ctx);
8329
+ if (echoed === parsed) return forwardData;
8330
+ return JSON.stringify(echoed);
8026
8331
  }
8027
8332
  /**
8028
8333
  * Handle streaming direct Anthropic response (passthrough SSE events)
@@ -8049,9 +8354,10 @@ async function handleDirectAnthropicStreamingResponse(opts) {
8049
8354
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") checkRepetition(event.delta.text);
8050
8355
  const forwardData = serverToolFilter.rewriteEvent(event, rawEvent.data);
8051
8356
  if (forwardData === null) continue;
8357
+ const echoedData = echoForwardData(forwardData, event.type, ctx);
8052
8358
  await stream.writeSSE({
8053
8359
  event: rawEvent.event || event.type,
8054
- data: forwardData
8360
+ data: echoedData
8055
8361
  });
8056
8362
  }
8057
8363
  recordAnthropicStreamingResponse(acc, anthropicPayload.model, ctx);
@@ -8231,7 +8537,7 @@ function handleNonStreamingResponse(opts) {
8231
8537
  toolCount: anthropicPayload.tools?.length ?? 0,
8232
8538
  stopReason: anthropicResponse.stop_reason ?? void 0
8233
8539
  });
8234
- return c.json(anthropicResponse);
8540
+ return c.json(echoResponseBody(anthropicResponse, ctx));
8235
8541
  }
8236
8542
  async function handleStreamingResponse(opts) {
8237
8543
  const { stream, response, toolNameMapping, anthropicPayload, ctx } = opts;
@@ -8255,7 +8561,8 @@ async function handleStreamingResponse(opts) {
8255
8561
  toolNameMapping,
8256
8562
  streamState,
8257
8563
  acc,
8258
- checkRepetition
8564
+ checkRepetition,
8565
+ ctx
8259
8566
  });
8260
8567
  recordAnthropicStreamingResponse(acc, anthropicPayload.model, ctx);
8261
8568
  completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens, ctx.queueWaitMs, void 0, {
@@ -8318,7 +8625,7 @@ async function sendTruncationMarkerEvent(stream, streamState, marker) {
8318
8625
  streamState.contentBlockIndex++;
8319
8626
  }
8320
8627
  async function processStreamChunks(opts) {
8321
- const { stream, response, toolNameMapping, streamState, acc, checkRepetition } = opts;
8628
+ const { stream, response, toolNameMapping, streamState, acc, checkRepetition, ctx } = opts;
8322
8629
  for await (const rawEvent of response) {
8323
8630
  consola.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
8324
8631
  if (rawEvent.data === "[DONE]") break;
@@ -8336,9 +8643,10 @@ async function processStreamChunks(opts) {
8336
8643
  consola.debug("Translated Anthropic event:", JSON.stringify(event));
8337
8644
  processAnthropicEvent(event, acc);
8338
8645
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") checkRepetition(event.delta.text);
8646
+ const echoed = echoParsedEvent(event, ctx);
8339
8647
  await stream.writeSSE({
8340
- event: event.type,
8341
- data: JSON.stringify(event)
8648
+ event: echoed.type,
8649
+ data: JSON.stringify(echoed)
8342
8650
  });
8343
8651
  }
8344
8652
  }
@@ -8357,6 +8665,7 @@ function resolveModelFromBetaHeader(model, betaHeader) {
8357
8665
  async function handleCompletion(c) {
8358
8666
  const anthropicPayload = await c.req.json();
8359
8667
  consola.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
8668
+ const requestedModel = captureRequestedModel(anthropicPayload.model);
8360
8669
  const betaHeader = c.req.header("anthropic-beta");
8361
8670
  anthropicPayload.model = resolveModelFromBetaHeader(anthropicPayload.model, betaHeader);
8362
8671
  logToolInfo(anthropicPayload);
@@ -8381,7 +8690,8 @@ async function handleCompletion(c) {
8381
8690
  system: extractSystemPrompt(anthropicPayload.system)
8382
8691
  }),
8383
8692
  trackingId,
8384
- startTime
8693
+ startTime,
8694
+ requestedModel
8385
8695
  };
8386
8696
  if (useDirectAnthropicApi) return handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiatorOverride);
8387
8697
  return handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOverride);
@@ -8545,13 +8855,23 @@ const createResponses = async (payload, { vision, initiator, resolvedModel }) =>
8545
8855
  //#endregion
8546
8856
  //#region src/routes/responses/stream-id-sync.ts
8547
8857
  const createStreamIdTracker = () => ({ outputItems: /* @__PURE__ */ new Map() });
8548
- const fixStreamIds = (data, event, tracker) => {
8858
+ /**
8859
+ * Rewrite a single Responses SSE event's `data`: synchronize item ids AND echo
8860
+ * the requester's model id into any nested `response.model` the event carries,
8861
+ * from one parse/serialize. Events whose payload has no `response.model` (text
8862
+ * deltas, the `error` event, …) round-trip with their model untouched
8863
+ * (AC-MALFORMED-SSE); unparseable/empty `data` is forwarded byte-for-byte.
8864
+ *
8865
+ * The echo is applied AFTER the handler's history/tracking has read the upstream
8866
+ * values from the same chunks, preserving the observability split (AC-OBS).
8867
+ */
8868
+ const fixStreamIds = (data, event, tracker, requestedModel) => {
8549
8869
  if (!data) return data;
8550
- const parsed = JSON.parse(data);
8870
+ const echoed = echoModelInParsedEvent(JSON.parse(data), requestedModel);
8551
8871
  switch (event) {
8552
- case "response.output_item.added": return handleOutputItemAdded(parsed, tracker);
8553
- case "response.output_item.done": return handleOutputItemDone(parsed, tracker);
8554
- default: return handleItemId(parsed, tracker);
8872
+ case "response.output_item.added": return handleOutputItemAdded(echoed, tracker);
8873
+ case "response.output_item.done": return handleOutputItemDone(echoed, tracker);
8874
+ default: return handleItemId(echoed, tracker);
8555
8875
  }
8556
8876
  };
8557
8877
  const handleOutputItemAdded = (parsed, tracker) => {
@@ -8764,6 +9084,7 @@ const TERMINAL_EVENTS = new Set([
8764
9084
  ]);
8765
9085
  const handleResponses = async (c) => {
8766
9086
  let payload = await c.req.json();
9087
+ const requestedModel = captureRequestedModel(payload.model);
8767
9088
  if (state.normalizeResponsesCallIds) payload = normalizeCallIds(payload);
8768
9089
  consola.debug("Responses request payload:", JSON.stringify(payload));
8769
9090
  const trackingId = c.get("trackingId");
@@ -8786,7 +9107,8 @@ const handleResponses = async (c) => {
8786
9107
  const ctx = {
8787
9108
  historyId,
8788
9109
  trackingId,
8789
- startTime
9110
+ startTime,
9111
+ requestedModel
8790
9112
  };
8791
9113
  const selectedModel = findModelById(payload.model);
8792
9114
  if (!(selectedModel?.supported_endpoints?.includes(RESPONSES_ENDPOINT) ?? false)) {
@@ -8827,7 +9149,7 @@ const handleResponses = async (c) => {
8827
9149
  const parsed = JSON.parse(rawData);
8828
9150
  if (typeof parsed.sequence_number === "number") lastSequenceNumber = parsed.sequence_number;
8829
9151
  } catch {}
8830
- const processedData = fixStreamIds(rawData, eventType, idTracker);
9152
+ const processedData = fixStreamIds(rawData, eventType, idTracker, requestedModel);
8831
9153
  await stream.writeSSE({
8832
9154
  id: chunk.id,
8833
9155
  event: eventType,
@@ -8890,7 +9212,7 @@ const handleResponses = async (c) => {
8890
9212
  toolCount: tools.length
8891
9213
  });
8892
9214
  consola.debug("Forwarding native Responses result:", JSON.stringify(result).slice(-400));
8893
- return c.json(result);
9215
+ return c.json(echoResponseBody(result, ctx));
8894
9216
  } catch (error) {
8895
9217
  recordErrorResponse(ctx, model, error, "responses", stream);
8896
9218
  failTracking(trackingId, error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!",
5
5
  "author": "dianshuv",
6
6
  "type": "module",