@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.
- package/dist/main.mjs +339 -62
- 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.
|
|
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
|
-
|
|
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
|
|
4368
|
-
if (!chunk.data || chunk.data === "[DONE]")
|
|
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
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
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
|
-
|
|
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
|
|
4482
|
-
|
|
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
|
|
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:
|
|
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:
|
|
8386
|
-
data: JSON.stringify(
|
|
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
|
-
|
|
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
|
|
8870
|
+
const echoed = echoModelInParsedEvent(JSON.parse(data), requestedModel);
|
|
8596
8871
|
switch (event) {
|
|
8597
|
-
case "response.output_item.added": return handleOutputItemAdded(
|
|
8598
|
-
case "response.output_item.done": return handleOutputItemDone(
|
|
8599
|
-
default: return handleItemId(
|
|
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);
|