mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-22 07:02:16 +03:00
Compare commits
1 Commits
fix/10990-
...
fix/10986-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9493a53edd |
1
changelog.d/fixes/10986-reasoning-only-content.md
Normal file
1
changelog.d/fixes/10986-reasoning-only-content.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(command-code): surface reasoning-only output as content when a model emits no text-delta (#10986)
|
||||
@@ -1 +0,0 @@
|
||||
- **Static model catalog for v0-vercel-web:** seed a static catalog for the v0-vercel-web web-cookie provider (v0-1.0-md, v0-1.5-lg, v0-1.5-md) so its dashboard "Available Models" / "Import from /models" UI serves a usable list instead of falling through to the route's 400 "does not support models listing" ([#10990](https://github.com/diegosouzapw/OmniRoute/issues/10990)).
|
||||
@@ -455,9 +455,10 @@
|
||||
"src/sse/handlers/chatHelpers.ts": 1019,
|
||||
"src/shared/middleware/chatBodyAdmission.ts": 1005,
|
||||
"_rebaseline_2026_08_20_10668_tabitoken_gateway": "#10668 (yawar-aquil) own catalog growth: src/shared/constants/providers/apikey/gateways.ts 1268->1283 (+15, entirely this PR diff -- one new tabitoken gateway entry, data lines only; base moved from 1255 to 1268 via other merges since the PR forked). Not combination drift: reproducible on the PR branch alone, so the WS5.5 release-captain rule does not apply. Extraction is not available -- the file is pure data (own header: \"Pure data; merged by apikey/index.ts via spread\") and already split into 6 family files under apikey/. Same precedent as _rebaseline_2026_08_14_imagetotext_servicekinds (#10275/#10291, gateways.ts 1250->1255, data lines only) and _rebaseline_2026_08_11_v3850_merge_storm_provider_registry (owner-authorized for this same file).",
|
||||
"open-sse/executors/commandCode.ts": 1038,
|
||||
"open-sse/executors/commandCode.ts": 1059,
|
||||
"_rebaseline_2026_08_21_10859_vision_bridge_catalog": "#10859 own growth (Vision Bridge fixes #10808/#10809): src/lib/modelCapabilities.ts 1006->1016 (+10, cmd/gpt-5.3-codex* text-only capability resolution) and open-sse/executors/commandCode.ts 988->1023 (+35, Command Code wire-model normalization for bare ids + reasoning field fallback for opencode-routed gateways). Cohesive bug fixes at the existing capability-resolution / executor chokepoints; not extractable mid-fix. Covered by tests/unit/model-capabilities-command-code-codex-textonly-10703.test.ts, tests/unit/command-code-vision.test.ts, tests/unit/opencode-mimo-reasoning-details-nonstream.test.ts. Pushed directly to release (own-session miss: the original rebaseline was made in a throwaway validation worktree and never landed on the PR branch or the release before merge).",
|
||||
"_rebaseline_2026_08_21_10907_sticky_pin_clear": "#10907 own growth: open-sse/executors/commandCode.ts 1023->1038 (+15, effort-suffix sanitization threading for the sticky-pin-clear fix). Cohesive change at the existing executor chokepoint. Covered by tests/unit/command-code-executor.test.ts."
|
||||
"_rebaseline_2026_08_21_10907_sticky_pin_clear": "#10907 own growth: open-sse/executors/commandCode.ts 1023->1038 (+15, effort-suffix sanitization threading for the sticky-pin-clear fix). Cohesive change at the existing executor chokepoint. Covered by tests/unit/command-code-executor.test.ts.",
|
||||
"_rebaseline_2026_08_21_10986_reasoning_only_content": "#10986 own growth: open-sse/executors/commandCode.ts 1038->1059 (+21, reasoning-only content fallback — when upstream emits only reasoning-delta events and never a text-delta, surface the reasoning text as message.content in createJsonResponse and emit a synthetic content delta in createStreamResponse). Cohesive bug fix at the existing executor chokepoint (mirrors precedent style of #10907/#10859). Covered by tests/unit/command-code-executor.test.ts (2 new cases: non-stream + streaming)."
|
||||
},
|
||||
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
|
||||
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",
|
||||
|
||||
@@ -738,6 +738,7 @@ function createStreamResponse(
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
let sentRole = false;
|
||||
let sentContent = false;
|
||||
let closed = false;
|
||||
const state: AggregateState = {
|
||||
content: "",
|
||||
@@ -772,7 +773,10 @@ function createStreamResponse(
|
||||
switch (event.type) {
|
||||
case "text-delta": {
|
||||
const text = stringValue(event.text) || "";
|
||||
if (text) controller.enqueue(sse(chatCompletionChunk(id, model, { content: text })));
|
||||
if (text) {
|
||||
sentContent = true;
|
||||
controller.enqueue(sse(chatCompletionChunk(id, model, { content: text })));
|
||||
}
|
||||
state.content += text;
|
||||
break;
|
||||
}
|
||||
@@ -810,6 +814,12 @@ function createStreamResponse(
|
||||
break;
|
||||
case "finish": {
|
||||
state.finishReason = mapFinishReason(event.finishReason);
|
||||
// If the model only produced reasoning-delta events (no text-delta), the
|
||||
// client-visible stream would otherwise end with no content. Emit one
|
||||
// content delta carrying the accumulated reasoning text (#10986).
|
||||
if (!sentContent && state.reasoning && state.toolCalls.length === 0) {
|
||||
controller.enqueue(sse(chatCompletionChunk(id, model, { content: state.reasoning })));
|
||||
}
|
||||
controller.enqueue(sse(chatCompletionChunk(id, model, {}, state.finishReason)));
|
||||
// Emit a standards-compliant usage-only chunk (choices: []) before
|
||||
// [DONE] when upstream reported usage. stream.ts's extractUsage
|
||||
@@ -857,6 +867,9 @@ function createStreamResponse(
|
||||
if (!closed) {
|
||||
if (!sentRole)
|
||||
controller.enqueue(sse(chatCompletionChunk(id, model, { role: "assistant" })));
|
||||
if (!sentContent && state.reasoning && state.toolCalls.length === 0) {
|
||||
controller.enqueue(sse(chatCompletionChunk(id, model, { content: state.reasoning })));
|
||||
}
|
||||
controller.enqueue(sse(chatCompletionChunk(id, model, {}, state.finishReason)));
|
||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
||||
controller.close();
|
||||
@@ -947,6 +960,14 @@ async function createJsonResponse(
|
||||
}
|
||||
|
||||
const message: JsonRecord = { role: "assistant", content: state.content };
|
||||
// Some Command Code models emit the whole answer as reasoning-delta events and
|
||||
// never a text-delta. When that leaves content empty, surface the reasoning text
|
||||
// as content too (#10986) so OpenAI-compatible clients get a usable answer. Keep
|
||||
// reasoning_content populated as well for reasoning-aware clients, and do not
|
||||
// override content when real text OR tool calls are present.
|
||||
if (!state.content && state.reasoning && state.toolCalls.length === 0) {
|
||||
message.content = state.reasoning;
|
||||
}
|
||||
if (state.reasoning) message.reasoning_content = state.reasoning;
|
||||
if (state.toolCalls.length > 0) message.tool_calls = state.toolCalls;
|
||||
|
||||
|
||||
@@ -99,15 +99,6 @@ const STATIC_MODEL_PROVIDERS: Record<string, () => Array<{ id: string; name: str
|
||||
{ id: "google_scholar", name: "Google Scholar" },
|
||||
{ id: "duckduckgo", name: "DuckDuckGo" },
|
||||
],
|
||||
"v0-vercel-web": () => [
|
||||
// v0-vercel-web web-cookie codegen provider — no upstream /v1/models endpoint,
|
||||
// no registry `models` and no discovery config, so seed the current v0 lineup
|
||||
// as a static catalog mirroring the v0-vercel API provider (shared.ts) so the
|
||||
// model-import UI serves a list instead of the tail 400 (#10990).
|
||||
{ id: "v0-1.0-md", name: "V0 1.0 MD" },
|
||||
{ id: "v0-1.5-lg", name: "V0 1.5 LG" },
|
||||
{ id: "v0-1.5-md", name: "V0 1.5 MD" },
|
||||
],
|
||||
"venice-web": () => [
|
||||
// Venice.ai web-cookie provider — no upstream /v1/models endpoint, so seed the
|
||||
// current lineup as a static catalog (#6269). Venice rotates its catalog; keep
|
||||
|
||||
@@ -333,6 +333,68 @@ test("Command Code data: SSE lines aggregate into non-stream ChatCompletion JSON
|
||||
});
|
||||
});
|
||||
|
||||
test("Command Code reasoning-only output falls back to reasoning as content (non-stream)", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
commandCodeStream(
|
||||
[
|
||||
{ type: "reasoning-delta", text: "The user wants 79874+93658. " },
|
||||
{ type: "reasoning-delta", text: "That equals 173532." },
|
||||
{
|
||||
type: "finish",
|
||||
finishReason: "stop",
|
||||
totalUsage: { inputTokens: 20, outputTokens: 64, outputTokenDetails: { reasoningTokens: 61 } },
|
||||
},
|
||||
],
|
||||
{ sse: true }
|
||||
);
|
||||
|
||||
const { response } = await getExecutor("command-code").execute({
|
||||
model: "meta/muse-spark-1.2-contributor",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: { messages: [{ role: "user", content: "Calculate 79874+93658, and reply with the result only." }] },
|
||||
});
|
||||
|
||||
const json = await response.json();
|
||||
const message = json.choices[0].message;
|
||||
// Regression #10986: when the model emits only reasoning-delta events (never a
|
||||
// text-delta), content must fall back to the reasoning text instead of "" (which
|
||||
// OpenAI-compatible clients treat as null/no answer).
|
||||
assert.equal(message.content, "The user wants 79874+93658. That equals 173532.");
|
||||
// reasoning_content must STAY populated for reasoning-aware clients.
|
||||
assert.equal(message.reasoning_content, "The user wants 79874+93658. That equals 173532.");
|
||||
});
|
||||
|
||||
test("Command Code reasoning-only output emits a content delta chunk when streaming", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
commandCodeStream(
|
||||
[
|
||||
{ type: "reasoning-delta", text: "The result is 173532." },
|
||||
{ type: "finish", finishReason: "stop" },
|
||||
],
|
||||
{ sse: true }
|
||||
);
|
||||
|
||||
const { response } = await getExecutor("command-code").execute({
|
||||
model: "meta/muse-spark-1.2-contributor",
|
||||
stream: true,
|
||||
credentials: { apiKey: "cc_test_key" },
|
||||
body: { messages: [{ role: "user", content: "Calcular 79874+93658" }] },
|
||||
});
|
||||
|
||||
const sse = await response.text();
|
||||
assert.match(sse, /data: \[DONE\]/);
|
||||
const chunks = parseSsePayloads(sse);
|
||||
assert.equal(chunks[0].choices[0].delta.role, "assistant");
|
||||
// Regression #10986: the reasoning-only stream must emit a content delta when it
|
||||
// otherwise ends with no content. reasoning_content stays present too.
|
||||
const contentDelta = chunks.find((c) => c.choices[0].delta.content !== undefined);
|
||||
assert.equal(contentDelta.choices[0].delta.content, "The result is 173532.");
|
||||
const reasoningDelta = chunks.find((c) => c.choices[0].delta.reasoning_content !== undefined);
|
||||
assert.equal(reasoningDelta.choices[0].delta.reasoning_content, "The result is 173532.");
|
||||
assert.equal(chunks.at(-1).choices[0].finish_reason, "stop");
|
||||
});
|
||||
|
||||
test("Command Code executor surfaces upstream and streamed errors", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
new Response("bad key", { status: 401, statusText: "Unauthorized" });
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* #10990 — v0-vercel-web (a web-cookie codegen provider) shipped a web-cookie
|
||||
* executor but no registry `models`, no discovery config, and no static-catalog
|
||||
* entry, so its "Import from /models" fell through to the models route's tail 400
|
||||
* ("Provider v0-vercel-web does not support models listing"). Adding a
|
||||
* `v0-vercel-web` entry to STATIC_MODEL_PROVIDERS gives the models route a local
|
||||
* catalog to serve (same class as the #6269 venice-web / #7820 amazon-q fix).
|
||||
*
|
||||
* Kept as a standalone unit against the pure `getStaticModelsForProvider` resolver
|
||||
* rather than extending the frozen `provider-models-route.test.ts` god-file.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { getStaticModelsForProvider } from "../../src/lib/providers/staticModels.ts";
|
||||
|
||||
test("#10990 v0-vercel-web resolves a non-empty static local catalog", () => {
|
||||
const models = getStaticModelsForProvider("v0-vercel-web");
|
||||
assert.ok(models && models.length > 0, "v0-vercel-web should expose a static catalog");
|
||||
const ids = models.map((m) => m.id);
|
||||
assert.ok(
|
||||
ids.includes("v0-1.5-lg"),
|
||||
`expected v0-1.5-lg in [${ids.join(", ")}]`
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user