@dianshuv/copilot-api 0.8.1 → 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 +339 -62
  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.1";
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*/;
@@ -3913,6 +4065,32 @@ function extractErrorMetrics(error) {
3913
4065
  * Shared utilities for request handlers.
3914
4066
  * Contains common functions used by both OpenAI and Anthropic message handlers.
3915
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
+ }
3916
4094
  /** Helper to update tracker model */
3917
4095
  function updateTrackerModel(trackingId, model, resolvedModel) {
3918
4096
  if (!trackingId) return;
@@ -4141,6 +4319,7 @@ function getReasoningTokensFromOpenAIUsage(usage) {
4141
4319
  async function handleCompletion$1(c) {
4142
4320
  const originalPayload = await c.req.json();
4143
4321
  consola.debug("Request payload:", JSON.stringify(originalPayload).slice(-400));
4322
+ const requestedModel = captureRequestedModel(originalPayload.model);
4144
4323
  const trackingId = c.get("trackingId");
4145
4324
  const startTime = (trackingId ? requestTracker.getRequest(trackingId) : void 0)?.startTime ?? Date.now();
4146
4325
  updateTrackerModel(trackingId, originalPayload.model);
@@ -4157,7 +4336,8 @@ async function handleCompletion$1(c) {
4157
4336
  temperature: originalPayload.temperature ?? void 0
4158
4337
  }),
4159
4338
  trackingId,
4160
- startTime
4339
+ startTime,
4340
+ requestedModel
4161
4341
  };
4162
4342
  const selectedModel = findModelById(originalPayload.model);
4163
4343
  await logTokenCount(originalPayload, selectedModel);
@@ -4262,7 +4442,7 @@ function handleNonStreamingResponse$1(c, originalResponse, ctx, payload) {
4262
4442
  reasoningTokens,
4263
4443
  stopReason: choice.finish_reason
4264
4444
  });
4265
- return c.json(response);
4445
+ return c.json(echoResponseBody(response, ctx));
4266
4446
  }
4267
4447
  function buildResponseContent(choice) {
4268
4448
  return {
@@ -4317,15 +4497,14 @@ async function handleStreamingResponse$1(opts) {
4317
4497
  }]
4318
4498
  };
4319
4499
  await stream.writeSSE({
4320
- data: JSON.stringify(markerChunk),
4500
+ data: JSON.stringify(echoParsedEvent(markerChunk, ctx)),
4321
4501
  event: "message"
4322
4502
  });
4323
4503
  acc.content += marker;
4324
4504
  }
4325
4505
  for await (const chunk of response) {
4326
4506
  consola.debug("Streaming chunk:", JSON.stringify(chunk));
4327
- parseStreamChunk(chunk, acc, checkRepetition);
4328
- await stream.writeSSE(chunk);
4507
+ await accumulateAndEchoChunk(chunk, acc, checkRepetition, ctx, stream);
4329
4508
  }
4330
4509
  recordStreamSuccess(acc, payload.model, ctx);
4331
4510
  completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens, ctx.queueWaitMs, acc.reasoningTokens, {
@@ -4358,45 +4537,70 @@ async function handleStreamingResponse$1(opts) {
4358
4537
  }]
4359
4538
  };
4360
4539
  await stream.writeSSE({
4361
- data: JSON.stringify(markerChunk),
4540
+ data: JSON.stringify(echoParsedEvent(markerChunk, ctx)),
4362
4541
  event: "message"
4363
4542
  });
4364
4543
  } catch {}
4365
4544
  }
4366
4545
  }
4367
- function parseStreamChunk(chunk, acc, checkRepetition) {
4368
- 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;
4369
4552
  try {
4370
- const parsed = JSON.parse(chunk.data);
4371
- if (parsed.model && !acc.model) acc.model = parsed.model;
4372
- if (parsed.usage) {
4373
- acc.inputTokens = parsed.usage.prompt_tokens;
4374
- acc.outputTokens = parsed.usage.completion_tokens;
4375
- acc.reasoningTokens = getReasoningTokensFromOpenAIUsage(parsed.usage) ?? 0;
4376
- }
4377
- const choice = parsed.choices[0];
4378
- if (choice) {
4379
- if (choice.delta.content) {
4380
- acc.content += choice.delta.content;
4381
- checkRepetition(choice.delta.content);
4382
- }
4383
- if (choice.delta.tool_calls) for (const tc of choice.delta.tool_calls) {
4384
- const idx = tc.index;
4385
- if (!acc.toolCallMap.has(idx)) acc.toolCallMap.set(idx, {
4386
- id: tc.id ?? "",
4387
- name: tc.function?.name ?? "",
4388
- arguments: ""
4389
- });
4390
- const item = acc.toolCallMap.get(idx);
4391
- if (item) {
4392
- if (tc.id) item.id = tc.id;
4393
- if (tc.function?.name) item.name = tc.function.name;
4394
- if (tc.function?.arguments) item.arguments += tc.function.arguments;
4395
- }
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;
4396
4600
  }
4397
- if (choice.finish_reason) acc.finishReason = choice.finish_reason;
4398
4601
  }
4399
- } catch {}
4602
+ if (choice.finish_reason) acc.finishReason = choice.finish_reason;
4603
+ }
4400
4604
  }
4401
4605
  function recordStreamSuccess(acc, fallbackModel, ctx) {
4402
4606
  for (const tc of acc.toolCallMap.values()) if (tc.id && tc.name) acc.toolCalls.push(tc);
@@ -4476,10 +4680,30 @@ const createEmbeddings = async (payload) => {
4476
4680
  //#endregion
4477
4681
  //#region src/routes/embeddings/route.ts
4478
4682
  const embeddingRoutes = new Hono();
4683
+ function isRecord(value) {
4684
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4685
+ }
4479
4686
  embeddingRoutes.post("/", async (c) => {
4687
+ const startTime = Date.now();
4480
4688
  try {
4481
- const response = await createEmbeddings(await c.req.json());
4482
- 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));
4483
4707
  } catch (error) {
4484
4708
  return forwardError(c, error);
4485
4709
  }
@@ -4831,7 +5055,7 @@ function buildUsageMetadata(usage) {
4831
5055
 
4832
5056
  //#endregion
4833
5057
  //#region src/routes/gemini/handler.ts
4834
- async function handleGeminiGenerate(c, model, isStream) {
5058
+ async function handleGeminiGenerate(c, model, isStream, requestedModel) {
4835
5059
  try {
4836
5060
  const geminiRequest = await c.req.json();
4837
5061
  consola.debug("Gemini request for model:", model, "stream:", isStream);
@@ -4856,7 +5080,8 @@ async function handleGeminiGenerate(c, model, isStream) {
4856
5080
  temperature: payload.temperature ?? void 0
4857
5081
  }),
4858
5082
  trackingId,
4859
- startTime
5083
+ startTime,
5084
+ requestedModel
4860
5085
  };
4861
5086
  const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createChatCompletions(payload, { resolvedModel: selectedModel }));
4862
5087
  ctx.queueWaitMs = queueWaitMs;
@@ -4879,7 +5104,7 @@ async function handleGeminiGenerate(c, model, isStream) {
4879
5104
  continue;
4880
5105
  }
4881
5106
  const geminiChunks = translateOpenAIChunkToGemini(chunk, streamState);
4882
- 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`);
4883
5108
  }
4884
5109
  recordResponse(ctx.historyId, {
4885
5110
  success: true,
@@ -4947,7 +5172,7 @@ function handleNonStreamResponse(c, response, model, ctx, payload) {
4947
5172
  stopReason: response.choices[0]?.finish_reason,
4948
5173
  toolCount: payload.tools?.length ?? 0
4949
5174
  });
4950
- return c.json(geminiResponse);
5175
+ return c.json(echoResponseBody(geminiResponse, ctx));
4951
5176
  }
4952
5177
 
4953
5178
  //#endregion
@@ -4983,11 +5208,13 @@ geminiRoutes.post("/:modelAction", async (c) => {
4983
5208
  const modelAction = c.req.param("modelAction");
4984
5209
  const colonIndex = modelAction.lastIndexOf(":");
4985
5210
  if (colonIndex === -1) return geminiError(c, 400, "INVALID_ARGUMENT", "Missing action in URL");
4986
- 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);
4987
5214
  const action = modelAction.slice(Math.max(0, colonIndex + 1));
4988
5215
  switch (action) {
4989
- case "generateContent": return handleGeminiGenerate(c, model, false);
4990
- 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);
4991
5218
  case "countTokens": return handleGeminiCountTokens(c, model);
4992
5219
  default: return geminiError(c, 400, "INVALID_ARGUMENT", `Unknown action: ${action}`);
4993
5220
  }
@@ -8067,7 +8294,40 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
8067
8294
  if (state.verbose && truncateResult?.wasCompacted) finalResponse = prependMarkerToResponse(response, createTruncationMarker$1(truncateResult));
8068
8295
  logServerToolBlocks(finalResponse.content);
8069
8296
  finalResponse = filterServerToolBlocksFromResponse(finalResponse);
8070
- 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);
8071
8331
  }
8072
8332
  /**
8073
8333
  * Handle streaming direct Anthropic response (passthrough SSE events)
@@ -8094,9 +8354,10 @@ async function handleDirectAnthropicStreamingResponse(opts) {
8094
8354
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") checkRepetition(event.delta.text);
8095
8355
  const forwardData = serverToolFilter.rewriteEvent(event, rawEvent.data);
8096
8356
  if (forwardData === null) continue;
8357
+ const echoedData = echoForwardData(forwardData, event.type, ctx);
8097
8358
  await stream.writeSSE({
8098
8359
  event: rawEvent.event || event.type,
8099
- data: forwardData
8360
+ data: echoedData
8100
8361
  });
8101
8362
  }
8102
8363
  recordAnthropicStreamingResponse(acc, anthropicPayload.model, ctx);
@@ -8276,7 +8537,7 @@ function handleNonStreamingResponse(opts) {
8276
8537
  toolCount: anthropicPayload.tools?.length ?? 0,
8277
8538
  stopReason: anthropicResponse.stop_reason ?? void 0
8278
8539
  });
8279
- return c.json(anthropicResponse);
8540
+ return c.json(echoResponseBody(anthropicResponse, ctx));
8280
8541
  }
8281
8542
  async function handleStreamingResponse(opts) {
8282
8543
  const { stream, response, toolNameMapping, anthropicPayload, ctx } = opts;
@@ -8300,7 +8561,8 @@ async function handleStreamingResponse(opts) {
8300
8561
  toolNameMapping,
8301
8562
  streamState,
8302
8563
  acc,
8303
- checkRepetition
8564
+ checkRepetition,
8565
+ ctx
8304
8566
  });
8305
8567
  recordAnthropicStreamingResponse(acc, anthropicPayload.model, ctx);
8306
8568
  completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens, ctx.queueWaitMs, void 0, {
@@ -8363,7 +8625,7 @@ async function sendTruncationMarkerEvent(stream, streamState, marker) {
8363
8625
  streamState.contentBlockIndex++;
8364
8626
  }
8365
8627
  async function processStreamChunks(opts) {
8366
- const { stream, response, toolNameMapping, streamState, acc, checkRepetition } = opts;
8628
+ const { stream, response, toolNameMapping, streamState, acc, checkRepetition, ctx } = opts;
8367
8629
  for await (const rawEvent of response) {
8368
8630
  consola.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
8369
8631
  if (rawEvent.data === "[DONE]") break;
@@ -8381,9 +8643,10 @@ async function processStreamChunks(opts) {
8381
8643
  consola.debug("Translated Anthropic event:", JSON.stringify(event));
8382
8644
  processAnthropicEvent(event, acc);
8383
8645
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") checkRepetition(event.delta.text);
8646
+ const echoed = echoParsedEvent(event, ctx);
8384
8647
  await stream.writeSSE({
8385
- event: event.type,
8386
- data: JSON.stringify(event)
8648
+ event: echoed.type,
8649
+ data: JSON.stringify(echoed)
8387
8650
  });
8388
8651
  }
8389
8652
  }
@@ -8402,6 +8665,7 @@ function resolveModelFromBetaHeader(model, betaHeader) {
8402
8665
  async function handleCompletion(c) {
8403
8666
  const anthropicPayload = await c.req.json();
8404
8667
  consola.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
8668
+ const requestedModel = captureRequestedModel(anthropicPayload.model);
8405
8669
  const betaHeader = c.req.header("anthropic-beta");
8406
8670
  anthropicPayload.model = resolveModelFromBetaHeader(anthropicPayload.model, betaHeader);
8407
8671
  logToolInfo(anthropicPayload);
@@ -8426,7 +8690,8 @@ async function handleCompletion(c) {
8426
8690
  system: extractSystemPrompt(anthropicPayload.system)
8427
8691
  }),
8428
8692
  trackingId,
8429
- startTime
8693
+ startTime,
8694
+ requestedModel
8430
8695
  };
8431
8696
  if (useDirectAnthropicApi) return handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiatorOverride);
8432
8697
  return handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOverride);
@@ -8590,13 +8855,23 @@ const createResponses = async (payload, { vision, initiator, resolvedModel }) =>
8590
8855
  //#endregion
8591
8856
  //#region src/routes/responses/stream-id-sync.ts
8592
8857
  const createStreamIdTracker = () => ({ outputItems: /* @__PURE__ */ new Map() });
8593
- 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) => {
8594
8869
  if (!data) return data;
8595
- const parsed = JSON.parse(data);
8870
+ const echoed = echoModelInParsedEvent(JSON.parse(data), requestedModel);
8596
8871
  switch (event) {
8597
- case "response.output_item.added": return handleOutputItemAdded(parsed, tracker);
8598
- case "response.output_item.done": return handleOutputItemDone(parsed, tracker);
8599
- 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);
8600
8875
  }
8601
8876
  };
8602
8877
  const handleOutputItemAdded = (parsed, tracker) => {
@@ -8809,6 +9084,7 @@ const TERMINAL_EVENTS = new Set([
8809
9084
  ]);
8810
9085
  const handleResponses = async (c) => {
8811
9086
  let payload = await c.req.json();
9087
+ const requestedModel = captureRequestedModel(payload.model);
8812
9088
  if (state.normalizeResponsesCallIds) payload = normalizeCallIds(payload);
8813
9089
  consola.debug("Responses request payload:", JSON.stringify(payload));
8814
9090
  const trackingId = c.get("trackingId");
@@ -8831,7 +9107,8 @@ const handleResponses = async (c) => {
8831
9107
  const ctx = {
8832
9108
  historyId,
8833
9109
  trackingId,
8834
- startTime
9110
+ startTime,
9111
+ requestedModel
8835
9112
  };
8836
9113
  const selectedModel = findModelById(payload.model);
8837
9114
  if (!(selectedModel?.supported_endpoints?.includes(RESPONSES_ENDPOINT) ?? false)) {
@@ -8872,7 +9149,7 @@ const handleResponses = async (c) => {
8872
9149
  const parsed = JSON.parse(rawData);
8873
9150
  if (typeof parsed.sequence_number === "number") lastSequenceNumber = parsed.sequence_number;
8874
9151
  } catch {}
8875
- const processedData = fixStreamIds(rawData, eventType, idTracker);
9152
+ const processedData = fixStreamIds(rawData, eventType, idTracker, requestedModel);
8876
9153
  await stream.writeSSE({
8877
9154
  id: chunk.id,
8878
9155
  event: eventType,
@@ -8935,7 +9212,7 @@ const handleResponses = async (c) => {
8935
9212
  toolCount: tools.length
8936
9213
  });
8937
9214
  consola.debug("Forwarding native Responses result:", JSON.stringify(result).slice(-400));
8938
- return c.json(result);
9215
+ return c.json(echoResponseBody(result, ctx));
8939
9216
  } catch (error) {
8940
9217
  recordErrorResponse(ctx, model, error, "responses", stream);
8941
9218
  failTracking(trackingId, error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.8.1",
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",