mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
* chore(release): open v3.8.21 development cycle
* fix: pass through valid max_tokens-truncated responses instead of fake 502 (#3572) (#3595)
* fix: /v1/completions returns legacy text-completion format, not chat (#3571) (#3596)
* fix: z.ai/GLM coding plan no longer shows Monthly 0% when no monthly cap (#3580) (#3597)
* docs: mark DISCOVERY_TOOL_DESIGN endpoints as Phase-2 not-yet-implemented (#3498) (#3599)
* fix(agent-bridge): add validate-only upstream-ca/test route (#3488) (#3600)
* fix(gamification): add level/badges/badges-earned profile routes (#3484)
* security(oauth): migrate 5 public client_ids to resolvePublicCred (#3493)
* fix(mcp): ship MCP server source closure in npm files + coverage gate (#3578)
* fix: add reasoning token buffer for combo routing (fixes #3587) (#3588)
Integrated into release/v3.8.21
* Refactor: Extract chatCore phases into modular files (#3598)
Integrated into release/v3.8.21 — chatCore phase modularization. Adjusted: re-derive idempotencyKey for the save path after the check moved into the module (co-authored). Thanks @oyi77!
* docs(changelog): credit #3598 (chatCore modularization) + #3588 (combo reasoning buffer)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(api): implement GET /api/guardrails + POST /api/guardrails/test, drop shadow/guardrails doc-fiction (#3496) (#3602)
Integrated into release/v3.8.21 — implements GET /api/guardrails + POST /api/guardrails/test, removes shadow/guardrails doc-fiction. TDD-validated (5/5) + check-docs-symbols/typecheck/eslint green.
* fix(gemini): isolate textual reasoning wrappers (#3605)
Split-out PR C from #3584. Isolates textual reasoning wrappers (<think>/<thinking>/<thought>/<internal_thought>, including malformed/open tags) into reasoning_content across both the non-streaming sanitizer and the Gemini streaming translator, with split-chunk buffering. Additive to the existing textual tool-call pipeline; does not touch the #3569 native functionResponse path. Integrated into release/v3.8.21. Thanks @dhaern!
* fix(antigravity): normalize Gemini 3.5 Flash tier IDs (#3603)
Split-out PR A from #3584. Normalizes the Antigravity/agy Gemini 3.5 Flash tier IDs to clean public names (gemini-3.5-flash-low/medium/high), maps them to the live upstream IDs at the executor boundary, and removes Antigravity from the global model resolver so the executor owns wire normalization. Maintainer follow-up: kept gemini-3.5-flash-preview as a hidden backward-compat alias routing to the High tier (so saved combos/configs keep working). Live-validated the tier set via the agy CLI catalog. Integrated into release/v3.8.21. Thanks @dhaern!
* fix(agent-bridge): surface real MITM startup-failure cause, not always port 443 (#3606) (#3608)
Integrated into release/v3.8.21 (#3606)
* fix(oauth): surface real Kiro import-token failure cause, not a bare 500 (#3589) (#3609)
Integrated into release/v3.8.21 (#3589)
* docs(opencode-provider): soft-deprecate in favor of @omniroute/opencode-plugin (#3419) (#3613)
Integrated into release/v3.8.21 (#3419)
* fix(usage): normalize Antigravity and agy provider quotas (#3604)
Split-out PR B from #3584. Normalizes Antigravity/agy provider quotas: prefers retrieveUserQuota for live consumption, falls back to fetchAvailableModels and local usage_history, sanitizes cached Provider Limits so retired upstream IDs are not re-exposed, and schedules a deduplicated post-usage refresh. Maintainer follow-up: decoupled the post-usage refresh via a lightweight usageEvents bus (usageHistory no longer dynamic-imports providerLimits) so it does not pull the executors/translator graph into the typecheck-core surface — typecheck:core stays at 0. Integrated into release/v3.8.21. Thanks @dhaern!
* feat(cli): add autostart on/off/toggle shorthand for headless serve mode (#3331) (#3614)
Integrated into release/v3.8.21 (#3331)
* docs(changelog): credit #3603 (Flash tier IDs) + #3604 (provider quotas) + #3605 (reasoning wrappers)
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(review): resolve findings from /review-reviews battery (v3.8.21 hardening) (#3618)
Pre-release hardening from the /review-reviews battery — 15 findings resolved (L1-L13,L15) + L14 live-verified WONTFIX, convergence re-review clean. lint/typecheck:core/test:vitest(146)/build green; zero new test:unit failures vs baseline 797de433f.
* chore(release): v3.8.21 CHANGELOG + i18n + env-doc sync
---------
Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Raxxoor <manker_lol@hotmail.com>
157 lines
6.4 KiB
TypeScript
157 lines
6.4 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
toTextCompletionObject,
|
|
transformSseData,
|
|
createTextCompletionStreamTransformer,
|
|
asTextCompletionResponse,
|
|
} from "../../src/app/api/v1/completions/textCompletionTransform.ts";
|
|
|
|
// #3571 — /v1/completions (legacy OpenAI Completions API) must return
|
|
// `object: "text_completion"` with `choices[].text`, not the chat shape
|
|
// (`chat.completion(.chunk)` with `choices[].message|delta.content`), which crashes
|
|
// TabbyML's `openai/completion` backend ("missing field `text`").
|
|
|
|
test("#3571 non-stream: chat.completion → text_completion with choices[].text", () => {
|
|
const out = toTextCompletionObject({
|
|
id: "chatcmpl-1",
|
|
object: "chat.completion",
|
|
created: 1,
|
|
model: "ds/deepseek-v4-flash",
|
|
choices: [
|
|
{ index: 0, message: { role: "assistant", content: "public class Test {}" }, finish_reason: "stop" },
|
|
],
|
|
usage: { prompt_tokens: 3, completion_tokens: 5, total_tokens: 8 },
|
|
});
|
|
assert.equal(out.object, "text_completion");
|
|
assert.equal(out.choices[0].text, "public class Test {}");
|
|
assert.equal(out.choices[0].finish_reason, "stop");
|
|
assert.equal(out.choices[0].index, 0);
|
|
assert.equal(out.choices[0].logprobs, null);
|
|
assert.equal(out.choices[0].message, undefined); // no chat shape leaks
|
|
assert.deepEqual(out.usage, { prompt_tokens: 3, completion_tokens: 5, total_tokens: 8 });
|
|
});
|
|
|
|
test("#3571 stream chunk: chat.completion.chunk(delta) → text_completion with text", () => {
|
|
const out = toTextCompletionObject({
|
|
id: "chatcmpl-2",
|
|
object: "chat.completion.chunk",
|
|
created: 2,
|
|
model: "gpt-5.5",
|
|
choices: [{ index: 0, delta: { content: "Hi" }, finish_reason: null }],
|
|
});
|
|
assert.equal(out.object, "text_completion");
|
|
assert.equal(out.choices[0].text, "Hi");
|
|
assert.equal(out.choices[0].delta, undefined);
|
|
});
|
|
|
|
test("#3571 transformSseData: passes [DONE] and non-JSON through, rewrites chat JSON", () => {
|
|
assert.equal(transformSseData("[DONE]"), "[DONE]");
|
|
assert.equal(transformSseData(" "), "");
|
|
assert.equal(transformSseData("not json"), "not json");
|
|
const rewritten = JSON.parse(
|
|
transformSseData('{"object":"chat.completion.chunk","choices":[{"delta":{"content":"X"}}]}')
|
|
);
|
|
assert.equal(rewritten.object, "text_completion");
|
|
assert.equal(rewritten.choices[0].text, "X");
|
|
});
|
|
|
|
test("#3571 empty delta content → text:'' (never undefined → no 'missing field text')", () => {
|
|
const out = toTextCompletionObject({
|
|
object: "chat.completion.chunk",
|
|
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
|
});
|
|
assert.equal(out.choices[0].text, "");
|
|
});
|
|
|
|
test("#3571 stream transformer end-to-end: chat SSE → text SSE", async () => {
|
|
const encoder = new TextEncoder();
|
|
const chatSse =
|
|
'data: {"object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}\n\n' +
|
|
'data: {"object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":" world"},"finish_reason":"stop"}]}\n\n' +
|
|
"data: [DONE]\n\n";
|
|
|
|
const source = new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
// split into two arbitrary byte chunks to exercise the line buffer across boundaries
|
|
const mid = Math.floor(chatSse.length / 2);
|
|
controller.enqueue(encoder.encode(chatSse.slice(0, mid)));
|
|
controller.enqueue(encoder.encode(chatSse.slice(mid)));
|
|
controller.close();
|
|
},
|
|
});
|
|
|
|
const out = source.pipeThrough(createTextCompletionStreamTransformer());
|
|
const reader = out.getReader();
|
|
const decoder = new TextDecoder();
|
|
let result = "";
|
|
for (;;) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
result += decoder.decode(value, { stream: true });
|
|
}
|
|
|
|
const dataLines = result
|
|
.split("\n")
|
|
.filter((l) => l.startsWith("data:") && !l.includes("[DONE]"))
|
|
.map((l) => JSON.parse(l.slice("data:".length).trim()));
|
|
|
|
assert.equal(dataLines.length, 2);
|
|
assert.ok(dataLines.every((o) => o.object === "text_completion"));
|
|
assert.equal(dataLines[0].choices[0].text, "Hello");
|
|
assert.equal(dataLines[1].choices[0].text, " world");
|
|
assert.equal(dataLines[1].choices[0].finish_reason, "stop");
|
|
assert.ok(result.includes("data: [DONE]")); // [DONE] preserved
|
|
assert.ok(!result.includes("delta")); // no chat shape leaks
|
|
});
|
|
|
|
// #3821-review LEDGER-8 — both response branches rewrite the body, so a stale upstream
|
|
// content-length must be dropped (a buffered SSE body with content-length would otherwise
|
|
// advertise the pre-rewrite length and truncate/hang the client).
|
|
test("#3571/#3821 asTextCompletionResponse drops content-length on the SSE branch", async () => {
|
|
const sseBody =
|
|
'data: {"object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"hi"},"finish_reason":"stop"}]}\n\n';
|
|
const upstream = new Response(sseBody, {
|
|
status: 200,
|
|
headers: {
|
|
"content-type": "text/event-stream",
|
|
// A (deliberately wrong) content-length that must NOT survive the rewrite.
|
|
"content-length": String(sseBody.length),
|
|
},
|
|
});
|
|
|
|
const out = await asTextCompletionResponse(upstream);
|
|
assert.equal(out.headers.get("content-length"), null, "content-length must be stripped");
|
|
assert.match(out.headers.get("content-type") || "", /text\/event-stream/);
|
|
const text = await out.text();
|
|
assert.ok(text.includes('"object":"text_completion"'));
|
|
assert.ok(text.includes('"text":"hi"'));
|
|
});
|
|
|
|
test("#3571/#3821 asTextCompletionResponse drops content-length on the JSON branch", async () => {
|
|
const jsonBody = JSON.stringify({
|
|
object: "chat.completion",
|
|
choices: [{ index: 0, message: { content: "hi" }, finish_reason: "stop" }],
|
|
});
|
|
const upstream = new Response(jsonBody, {
|
|
status: 200,
|
|
headers: { "content-type": "application/json", "content-length": String(jsonBody.length) },
|
|
});
|
|
|
|
const out = await asTextCompletionResponse(upstream);
|
|
assert.equal(out.headers.get("content-length"), null);
|
|
const obj = await out.json();
|
|
assert.equal(obj.object, "text_completion");
|
|
assert.equal(obj.choices[0].text, "hi");
|
|
});
|
|
|
|
test("#3571/#3821 asTextCompletionResponse passes error responses through untouched", async () => {
|
|
const upstream = new Response(JSON.stringify({ error: { message: "boom" } }), {
|
|
status: 500,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
const out = await asTextCompletionResponse(upstream);
|
|
assert.equal(out, upstream, "non-ok responses are returned as-is");
|
|
});
|